Python functions exercises — grades, a mini bank and dice with statistics (and the return that gave up on the first roll)

While preparing the dice game for this article, I wanted to add a message at the end: “has any double come up in the game?”. I wrote a has_double function in a minute, tried it with the sample game… and it said False. With a 6 and a 6 on the third roll, right in front of me. I checked the condition, checked the data, added prints… and the bug was a single misplaced word: a return False inside an else, which made the function give up on the first roll that wasn’t a double. You’ll find it exactly as it was in the “What’s wrong here?” section, because it’s one of those errors that never shows a message.

You’ve seen the Python functions theory and the three programs in the practice. Now it’s your turn: four exercises across three levels (a grade converter, a report, a mini bank and a dice game with statistics), three snippets with a hidden bug and, at the end, a cheat sheet to keep everything at hand. The thread that ties them together is the quietest error in the topic: print where return was needed, which leaves you working with None without realising.

How to use this article

  1. Read the brief and the expected output. Your program must produce exactly that output (in the two random ones, thanks to a fixed seed).
  2. Try to solve it in VS Code or Thonny without looking at anything else. If you’ve been stuck for more than 10 minutes, read the hint.
  3. Before looking at the solutions, do the “What’s wrong here?” section: predict what comes out and check.
  4. Compare with the commented solution. Working isn’t enough: look at why it’s built that way and which error the obvious alternative would cause.

None of them uses input(): the data is written in the program, as in the practice. That way you can compare your output with the article’s line by line.

Basic level

Exercise 1: grade converter between systems

Write four functions that convert grades between three systems: the Spanish one (0-10), the US one (0-100) and letters (A, B, C, D, F):

  • spanish_to_us(grade): multiplies by 10.
  • us_to_spanish(grade): divides by 10.
  • spanish_to_letter(grade): from 9 to 10, A; from 7 to under 9, B; from 5 to under 7, C; from 3 to under 5, D; below 3, F.
  • letter_to_spanish(letter): A = 9.5, B = 7.5, C = 5.5, D = 3.5, F = 1.5 (the middle of each band), in upper or lower case, and None if the letter doesn’t exist.

A fifth function, show_conversions(grade), shows the three versions of a grade. The program checks that the grade is between 0 and 10, shows it and ends with two conversions back. With grade = 7.5, the output must be:

=== GRADE CONVERTER ===
--- Conversions ---
Spanish system: 7.5
US system: 75.0
Letter system: B
Back: 75 → 7.5 · b → 7.5

Each conversion function returns its result with return; only show_conversions uses print. If a conversion printed, us_grade = spanish_to_us(7.5) would be None even though you saw 75.0 on the screen. Round to one decimal with round(…, 1).

Exercise 2: grade report generator

Using two functions from the previous exercise, write a program that receives the grades of four subjects as a list of (name, grade) pairs and generates a report: each subject in the three systems, the average with its letter and the highest and lowest grade with their subject.

subjects = [("FP1", 8.5), ("Statistics", 7.0), ("Maths", 6.5), ("Databases", 9.0)]

Expected output:

=== GRADE REPORT ===
--- Full report ---
FP1 → 8.5 | 85.0 | B
Statistics → 7.0 | 70.0 | B
Maths → 6.5 | 65.0 | C
Databases → 9.0 | 90.0 | A
Average: 7.75 → B
Highest: 9.0 (Databases)
Lowest: 6.5 (Maths)

Create report_line(name, grade) that returns a subject’s line (not prints it) and generate_report(subjects) that uses it inside a for name, grade in subjects:. For the highest and lowest, start with an impossible value (−1 and 11) and keep updating it. Careful: FP1, with 8.5, is a B, not an A.

Intermediate level

Exercise 3: mini bank with functions

Simulate a bank account with functions: check_balance(balance), deposit(balance, amount), withdraw(balance, amount) and show_history(history, balance). The account starts at 1000.0 € and, in this order, there’s a deposit of 500, a withdrawal of 200, a withdrawal of 2000 (not enough money) and a deposit of −50 (invalid amount). The history only records the transactions that worked.

Expected output:

=== SERGIO LEARNS BANK ===
Current balance: 1000.0 €
✓ Deposit made: +500 €
✓ Withdrawal made: -200 €
Error: insufficient balance (you have 1300.0 €)
Error: the amount must be positive
--- Transaction history ---
Opening balance: +1000.0 €
Deposit: +500 €
Withdrawal: -200 €
──────────────────────────────
Current balance: 1300.0 €

deposit and withdraw receive the balance and return the new balance; if something fails, they return the same one they received. That’s the key return: if you forget it, balance = deposit(balance, 500) leaves the balance as None. To know whether to record the transaction, compare the balance before and after.

Final challenge

Exercise 4: dice game with statistics

Simulate six rolls of two dice. Each roll is shown with its sum and a notice if it’s a double. At the end, the statistics: total rolls, total sum, average, highest and lowest (with the roll where they first came up), number of doubles with its percentage and a distribution with █ bars, from the lowest sum to the highest, saying “1 time” or “2 times” as appropriate. Put random.seed(37) at the start so your dice come out like these:

=== DICE GAME ===
Roll 1: 🎲 6 + 🎲 5 = 11
Roll 2: 🎲 1 + 🎲 5 = 6
Roll 3: 🎲 6 + 🎲 6 = 12 ← Double!
Roll 4: 🎲 6 + 🎲 5 = 11
Roll 5: 🎲 1 + 🎲 6 = 7
Roll 6: 🎲 3 + 🎲 4 = 7
--- Statistics ---
Total rolls: 6
Total sum: 54
Average per roll: 9.0
Highest rolled: 12 (roll 3)
Lowest rolled: 6 (roll 2)
Doubles rolled: 1 (16.7 %)
Distribution of results:
   6: █ (1 time)
   7: ██ (2 times)
  11: ██ (2 times)
  12: █ (1 time)

roll_die() returns random.randint(1, 6) and roll_two_dice() returns two values (return roll_die(), roll_die()), collected with d1, d2 = roll_two_dice(). Store the sums in a list with append, find the highest and lowest with functions that return the value and the position, and for the distribution go through range(2, 13) counting with sums.count(value). The plural can be a function too.

What’s wrong here?

Three snippets that look right. For each one, predict what comes out before reading the explanation.

Snippet 1: the grade you see but don’t have

def spanish_to_us(grade):
    print(grade * 10)                # shows 75.0... and returns None


us_grade = spanish_to_us(7.5)
print("With 5 bonus points:", us_grade + 5)

What do you think comes out?

This:

75.0
Traceback (most recent call last):
  File "print_return.py", line 6, in <module>
    print("With 5 bonus points:", us_grade + 5)
                                  ~~~~~~~~~^~~
TypeError: unsupported operand type(s) for +: 'NoneType' and 'int'

The 75.0 appears, so the conversion “works”. But spanish_to_us has no return, so it returns None, and None + 5 is impossible: unsupported operand type(s) for +: 'NoneType' and 'int'. When you see NoneType in an error, the first thing to think is “some function has no return“.

Trusting the screen. The number appearing doesn’t mean the function returns it: print sends it to the screen and the variable gets None.

Fixed, with return:

def spanish_to_us(grade):
    return grade * 10                # returns the number to the caller


us_grade = spanish_to_us(7.5)
print("With 5 bonus points:", us_grade + 5)
With 5 bonus points: 80.0

7.5 × 10 = 75.0 and 75.0 + 5 = 80.0. Notice that now the 75.0 does not appear on its own: nobody prints it, it’s just used.

Conversion functions should almost never print. If you want to see the value while coding, print it outside: print(spanish_to_us(7.5)).

Snippet 2: the balance nobody can find

balance = 1000.0


def deposit(amount):
    balance = balance + amount        # the outer one? Python doesn't see it that way
    return balance


print(deposit(500))

What do you think comes out? 1500.0?

This:

Traceback (most recent call last):
  File "global_balance.py", line 9, in <module>
    print(deposit(500))
          ~~~~~~~^^^^^
  File "global_balance.py", line 5, in deposit
    balance = balance + amount        # the outer one? Python doesn't see it that way
              ^^^^^^^
UnboundLocalError: cannot access local variable 'balance' where it is not associated with a value

Since deposit assigns balance = …, Python decides balance is a local variable of the function, in its whole body. When it calculates balance + amount it tries to read that local, which has no value yet: UnboundLocalError. The global 1000.0 exists, but the function no longer looks at it.

Thinking a function can read an outer variable and change it at the same time without saying anything. Reading it, yes; assigning it, Python makes it local.

The fix isn’t global balance, but making the data go in and out in plain sight:

def deposit(balance, amount):
    return balance + amount           # goes in as a parameter, comes out through return


balance = 1000.0
balance = deposit(balance, 500)       # and it is stored outside, in plain sight
print(balance)
1500.0

That’s exactly how the mini bank in exercise 3 works: the balance goes in as a parameter and comes out through return.

Snippet 3: the return that gave up

The one from the story. The game has a double on the third roll:

def has_double(rolls):
    """Has any double come up in the game?"""
    for d1, d2 in rolls:
        if d1 == d2:
            return True
        else:
            return False            # ← watch this return


rolls = [(6, 5), (1, 5), (6, 6)]
print(has_double(rolls))

What do you think comes out?

This:

False

On the first pass, the roll is (6, 5): not a double, so it goes into the else and does return False. And return doesn’t end the pass, it ends the function. The rolls (1, 5) and (6, 6) are never checked. The diagram compares both versions with the same game:

The return that leaves on the first passThe rolls are (6, 5), (1, 5) and (6, 6). The version with else returns False on the first roll and never looks at the other two. The fixed version keeps going on the first two and returns True on the third.has_double (with else)(6, 5)return False(1, 5)never checked(6, 6)never checkedFalsehas_double (fixed)(6, 5)not a double: go on(1, 5)not a double: go on(6, 6)return TrueTrueafter the for: return False
A return inside the loop ends the whole function: the False can only go after the for, once every roll has been checked.

Writing the “not found” inside the loop. To say there is no double you must have checked every roll, and you only know that when the loop ends.

Fixed: the return False leaves the loop and the else:

def has_double(rolls):
    """Has any double come up in the game?"""
    for d1, d2 in rolls:
        if d1 == d2:
            return True             # as soon as one appears, we know the answer
    return False                    # only after checking ALL the rolls


rolls = [(6, 5), (1, 5), (6, 6)]
print(has_double(rolls))
True

In searches, the pattern is always the same: return as soon as you find what you’re looking for, inside the loop; and the “it isn’t there” return, after the loop. It’s the same shape as the tray search in the theory.

Commented solutions

Solution to exercise 1

# Basic level · Exercise 1: grade converter between systems
def spanish_to_us(grade):
    """0-10 → 0-100."""
    return round(grade * 10, 1)


def us_to_spanish(grade):
    """0-100 → 0-10."""
    return round(grade / 10, 1)


def spanish_to_letter(grade):
    """9-10 A · 7-8.9 B · 5-6.9 C · 3-4.9 D · below 3 F."""
    if grade >= 9:
        return "A"
    elif grade >= 7:
        return "B"
    elif grade >= 5:
        return "C"
    elif grade >= 3:
        return "D"
    return "F"


def letter_to_spanish(letter):
    """Each letter is worth the middle of its band; None if the letter doesn't exist."""
    letter = letter.upper()
    if letter == "A":
        return 9.5
    if letter == "B":
        return 7.5
    if letter == "C":
        return 5.5
    if letter == "D":
        return 3.5
    if letter == "F":
        return 1.5
    return None


def show_conversions(grade):
    """The only one that prints: takes the grade and shows the three versions."""
    print("--- Conversions ---")
    print("Spanish system:", grade)
    print("US system:", spanish_to_us(grade))
    print("Letter system:", spanish_to_letter(grade))


print("=== GRADE CONVERTER ===")
grade = 7.5                             # the grade that used to be asked with input()
if grade < 0 or grade > 10:
    print("Error: the grade must be between 0 and 10")
else:
    show_conversions(grade)
print("Back: 75 →", us_to_spanish(75), "· b →", letter_to_spanish("b"))

Output:

=== GRADE CONVERTER ===
--- Conversions ---
Spanish system: 7.5
US system: 75.0
Letter system: B
Back: 75 → 7.5 · b → 7.5

Why it’s built this way:

  • Four functions that return and one that prints. The conversions can be reused in exercise 2 precisely because they print nothing.
  • The bands with >= in descending order. spanish_to_letter(7.5): 7.5 >= 9 is false, 7.5 >= 7 is true → "B". Since the elifs are checked top to bottom, each one only needs the lower limit. If you wrote them bottom up (grade >= 3 first), a 9.5 would come out as D.
  • letter.upper() before comparing, so "b" and "B" are worth the same; if the letter doesn’t exist, it reaches the final return None.
  • Plain ASCII names. In the Spanish version of this article the functions are called espanola_… and not española_…: Python 3 accepts the ñ in names, but a classmate may not be able to type it and some tools complain. It’s a healthy habit.
  • The range check, outside the conversions. The conversions don’t decide what to do with an impossible grade; the main program does.

Sums: 7.5 × 10 = 75.0; 75 / 10 = 7.5; "b" → "B" → 7.5.

Solution to exercise 2

# Basic level · Exercise 2: grade report (reuses two functions from exercise 1)
def spanish_to_us(grade):
    return round(grade * 10, 1)


def spanish_to_letter(grade):
    if grade >= 9:
        return "A"
    elif grade >= 7:
        return "B"
    elif grade >= 5:
        return "C"
    elif grade >= 3:
        return "D"
    return "F"


def report_line(name, grade):
    """RETURNS the line for one subject (it doesn't print it: that way it can be reused)."""
    return f"{name} → {grade} | {spanish_to_us(grade)} | {spanish_to_letter(grade)}"


def generate_report(subjects):
    print("--- Full report ---")
    total_sum = 0
    how_many = 0
    grade_max, name_max = -1, ""     # we start below any possible grade...
    grade_min, name_min = 11, ""     # ...and above, so the first one already wins
    for name, grade in subjects:
        print(report_line(name, grade))
        total_sum += grade
        how_many += 1
        if grade > grade_max:        # > and not >=: on a tie the first one stays
            grade_max, name_max = grade, name
        if grade < grade_min:
            grade_min, name_min = grade, name
    average = round(total_sum / how_many, 2)
    print("Average:", average, "→", spanish_to_letter(average))
    print(f"Highest: {grade_max} ({name_max})")
    print(f"Lowest: {grade_min} ({name_min})")


print("=== GRADE REPORT ===")
generate_report([("FP1", 8.5), ("Statistics", 7.0), ("Maths", 6.5), ("Databases", 9.0)])

Output:

=== GRADE REPORT ===
--- Full report ---
FP1 → 8.5 | 85.0 | B
Statistics → 7.0 | 70.0 | B
Maths → 6.5 | 65.0 | C
Databases → 9.0 | 90.0 | A
Average: 7.75 → B
Highest: 9.0 (Databases)
Lowest: 6.5 (Maths)

Why it’s built this way:

  • report_line returns the text. generate_report prints it, but the same function would work for writing the report to a file.
  • The two functions from exercise 1, copied. They’re repeated here because each exercise is a separate file. Copying functions between files is exactly what the next topic solves, Python modules: you’d write from grade_converter import spanish_to_letter and that’s it.
  • Impossible values to start with. grade_max = -1 guarantees that the first grade (anything from 0 to 10) is already higher; grade_min = 11, that it’s lower.
  • > and not >=. On a tie, the first subject that reached that grade stays.
  • for name, grade in subjects unpacks each pair into two variables, just like euros, cents = … in the theory.

Trace of the highest and lowest:

PassSubjectgradetotal_sumgrade_max (name_max)grade_min (name_min)
start——0−111
1FP18.58.58.5 (FP1)8.5 (FP1)
2Statistics7.015.58.5 (FP1)7.0 (Statistics)
3Maths6.522.08.5 (FP1)6.5 (Maths)
4Databases9.031.09.0 (Databases)6.5 (Maths)

Average: 31.0 / 4 = 7.75, which is between 7 and 9 → B.

Solution to exercise 3

# Intermediate level · Exercise 3: mini bank with functions
def check_balance(balance):
    print(f"Current balance: {balance} €")


def deposit(balance, amount):
    """Returns the NEW balance. If the amount is not valid, returns the same balance."""
    if amount <= 0:
        print("Error: the amount must be positive")
        return balance
    print(f"✓ Deposit made: +{amount} €")
    return round(balance + amount, 2)      # the key return: without it, the balance never changes


def withdraw(balance, amount):
    """Returns the new balance, or the same one if there isn't enough money."""
    if amount <= 0:
        print("Error: the amount must be positive")
        return balance
    if amount > balance:
        print(f"Error: insufficient balance (you have {balance} €)")
        return balance
    print(f"✓ Withdrawal made: -{amount} €")
    return round(balance - amount, 2)


def record(history, text, balance_before, balance_after):
    """Records the transaction only if the balance changed (if there was an error, it doesn't)."""
    if balance_after != balance_before:
        history.append(text)


def show_history(history, balance):
    print("--- Transaction history ---")
    for transaction in history:
        print(transaction)
    print("─" * 30)
    check_balance(balance)


print("=== SERGIO LEARNS BANK ===")
balance = 1000.0
history = [f"Opening balance: +{balance} €"]
check_balance(balance)

before = balance
balance = deposit(balance, 500)                 # we store what the function RETURNS
record(history, "Deposit: +500 €", before, balance)

before = balance
balance = withdraw(balance, 200)
record(history, "Withdrawal: -200 €", before, balance)

before = balance
balance = withdraw(balance, 2000)               # there isn't that much: the balance doesn't change
record(history, "Withdrawal: -2000 €", before, balance)

before = balance
balance = deposit(balance, -50)                 # invalid amount
record(history, "Deposit: -50 €", before, balance)

show_history(history, balance)

Output:

=== SERGIO LEARNS BANK ===
Current balance: 1000.0 €
✓ Deposit made: +500 €
✓ Withdrawal made: -200 €
Error: insufficient balance (you have 1300.0 €)
Error: the amount must be positive
--- Transaction history ---
Opening balance: +1000.0 €
Deposit: +500 €
Withdrawal: -200 €
──────────────────────────────
Current balance: 1300.0 €

Why it’s built this way:

  • The balance is never global for the functions. It goes in as a parameter and comes out through return, and the main program reassigns it: balance = deposit(balance, 500). It’s the fix from snippet 2 applied to a whole program.
  • If something fails, the same balance comes back. That way the caller doesn’t have to tell cases apart: it always gets a valid balance.
  • record compares before and after. If the balance didn’t change, the operation failed and isn’t recorded. With the withdrawal of 2000 and the deposit of −50, the history stays at three lines.
  • The history is a list that record changes with append. Lists can be changed “from inside” by a function without returning them; numbers can’t. You’ll see it properly in the mutability topic; for now, notice that the balance (a number) needs the return and the history (a list) doesn’t.
  • round(…, 2) in the operations, so a balance like 0.1 + 0.2 doesn’t end up with decimal tails.

The diagram follows the balance from function to function. Each violet pill is what comes out of a return and goes into the next call:

The balance goes from function to functionThe balance starts at 1000.0. deposit(balance, 500) returns 1500.0; withdraw(balance, 200) returns 1300.0; withdraw(balance, 2000) fails and returns 1300.0; deposit(balance, -50) fails and returns 1300.0.balance = deposit(balance, 500) …1000.0deposit(balance, 500)  returns the new one1500.0withdraw(balance, 200)  returns the new one1300.01300.0withdraw(balance, 2000)  error: returns the same1300.0deposit(balance, -50)  error: returns the same1300.0
The balance goes in as a parameter and comes out through return; if the operation fails, the same one that went in comes out and the history doesn't record it.

Sums: 1000.0 + 500 = 1500.0; 1500.0 − 200 = 1300.0; 2000 > 1300.0, so the withdrawal fails; −50 ≤ 0, so the deposit fails. Final balance: 1300.0 €.

Solution to exercise 4

# Challenge · Exercise 4: dice game with statistics
import random

random.seed(37)    # fixed seed so the output is always the same (remove it to play)


def roll_die():
    return random.randint(1, 6)          # return: sends the number back, doesn't print it


def roll_two_dice():
    return roll_die(), roll_die()        # two values at once (a tuple)


def is_double(die1, die2):
    return die1 == die2


def highest_and_position(sums):
    """Returns the highest value and the roll it came up on (the first one if repeated)."""
    best, position, i = 0, 0, 0
    for s in sums:
        i += 1
        if s > best:
            best, position = s, i
    return best, position


def lowest_and_position(sums):
    worst, position, i = 13, 0, 0        # 13: more than the highest possible sum (6 + 6)
    for s in sums:
        i += 1
        if s < worst:
            worst, position = s, i
    return worst, position


def times(n):
    """«1 time» or «2 times»: the plural is a decision too."""
    if n == 1:
        return "1 time"
    return f"{n} times"


def show_distribution(sums):
    print("Distribution of results:")
    for value in range(2, 13):           # every possible sum, already in order
        n = sums.count(value)            # how many times that value came up
        if n > 0:
            print(f"  {value:2d}: {'█' * n} ({times(n)})")


def play(rolls):
    print("=== DICE GAME ===")
    sums = []
    doubles = 0
    for i in range(1, rolls + 1):
        d1, d2 = roll_two_dice()         # we unpack the tuple
        total_sum = d1 + d2
        notice = ""
        if is_double(d1, d2):
            notice = " ← Double!"
            doubles += 1
        print(f"Roll {i}: 🎲 {d1} + 🎲 {d2} = {total_sum}{notice}")
        sums.append(total_sum)
    highest, roll_max = highest_and_position(sums)
    lowest, roll_min = lowest_and_position(sums)
    print("--- Statistics ---")
    print("Total rolls:", rolls)
    print("Total sum:", sum(sums))
    print("Average per roll:", round(sum(sums) / rolls, 2))
    print(f"Highest rolled: {highest} (roll {roll_max})")
    print(f"Lowest rolled: {lowest} (roll {roll_min})")
    print(f"Doubles rolled: {doubles} ({round(doubles / rolls * 100, 1)} %)")
    show_distribution(sums)


play(6)

Output:

=== DICE GAME ===
Roll 1: 🎲 6 + 🎲 5 = 11
Roll 2: 🎲 1 + 🎲 5 = 6
Roll 3: 🎲 6 + 🎲 6 = 12 ← Double!
Roll 4: 🎲 6 + 🎲 5 = 11
Roll 5: 🎲 1 + 🎲 6 = 7
Roll 6: 🎲 3 + 🎲 4 = 7
--- Statistics ---
Total rolls: 6
Total sum: 54
Average per roll: 9.0
Highest rolled: 12 (roll 3)
Lowest rolled: 6 (roll 2)
Doubles rolled: 1 (16.7 %)
Distribution of results:
   6: █ (1 time)
   7: ██ (2 times)
  11: ██ (2 times)
  12: █ (1 time)

Why it’s built this way:

  • roll_die returns, it doesn’t print. If it printed, d1 + d2 would be None + None.
  • roll_two_dice returns a tuple that is unpacked into d1, d2. One function, two values.
  • highest_and_position and lowest_and_position return two things: the value and the roll. With > (and <) they keep the first time: 11 comes up on rolls 1 and 4, but the highest is the 12 on roll 3; and the lowest, the 6 on roll 2.
  • range(2, 13) for the distribution. It goes through every possible sum already in order, so there’s nothing to sort; sums.count(value) says how many times it came up and the if n > 0 skips the ones that didn’t.
  • times(n), a tiny function that avoids the “(2 time)” of the old version of this exercise.
  • random.seed(37) so the output is always the same. Delete it to play for real.

Trace of the statistics:

RollDiceSumDouble?Highest so farLowest so far
16 + 511no11 (1)11 (1)
21 + 56no11 (1)6 (2)
36 + 612yes12 (3)6 (2)
46 + 511no12 (3)6 (2)
51 + 67no12 (3)6 (2)
63 + 47no12 (3)6 (2)

Sum: 11 + 6 + 12 + 11 + 7 + 7 = 54; average 54 / 6 = 9.0; doubles 1 out of 6 = 16.7 %.


Exam-style quiz

Now test yourself as in the exam. The FP1 quizzes look like this: 15 questions in 20 minutes (16 in 30 in the Sequences one), almost all single choice (each wrong answer subtracts a third of what the question is worth) and many «what does this code print?», with the odd fill in the gaps, matching or true or false. Here are 13 original questions on functions. Answer without running anything, press Check and read the explanation for each one. These questions are also the question bank for the FP1 theory mock exam, which will mix them with those from the other topics.

Question 1Short answer

What does this program print? Write the exact output.

def double_plus(x, y=1):
    return 2 * x + y

print(double_plus(4) + double_plus(3, 5))
Solution

Answer: 20

double_plus(4) uses the default value y = 1: 2 × 4 + 1 = 9. double_plus(3, 5) gives 2 × 3 + 5 = 11. Total: 9 + 11 = 20.

Question 2Single choice

What does this code print?

def greet(name):
    print("Hello", name)

r = greet("Luis")
print(r)
  1. Hello Luis and then None
  2. Hello Luis twice
  3. Only Hello Luis
  4. An error
Solution

Answer: Hello Luis and then None

print shows and return gives back. greet shows the greeting but returns nothing, so r is None. Mixing them up is the most common mistake of the topic.

Question 3Match

In def area(base, height): … area(4, 3), match each term:

base and height
4 and 3
area
area(4, 3)

Definitions:

  • Call to the function
  • Formal parameters
  • Arguments (actual parameters)
  • Default value
  • Name of the function
Solution
  • base and height → Formal parameters
  • 4 and 3 → Arguments (actual parameters)
  • area → Name of the function
  • area(4, 3) → Call to the function

Parameters are the names that appear in the definition; arguments, the actual values you pass when calling it. In the call, each argument is assigned to its parameter in order.

Question 4Single choice

What is it called when you pass the arguments like this: area(height=3, base=4)?

  1. Passing by name (keyword arguments)
  2. Passing by position
  3. Default values
  4. Recursion
Solution

Answer: Passing by name (keyword arguments)

With name=value you tie each argument to its parameter explicitly. It is useful when a function has many parameters or some with default values.

Question 5Short answer

What does this program print? Write the exact output.

def f(n):
    if n > 10:
        return "big"
    return "small"
    print("end")

print(f(15))
Solution

Answer: big

return ends the function right there. With 15 the if holds and it returns big. The print("end") comes after a return, so it never runs.

Question 6Single choice

What does this code print?

x = 5

def change():
    x = 99
    return x

change()
print(x)
  1. 5
  2. 99
  3. An error, because x already exists
  4. None
Solution

Answer: 5

Each call has its own scope: x = 99 creates a local x that disappears when the function ends. The global x is still 5. To get a value out of a function, return it.

Question 7True or false

About functions:

  1. A function can return several values separated by commas: return a, b.
  2. A function runs as soon as Python reads its def.
  3. A function can call another function.
  4. Parameters with a default value can come before those without one: def f(a=1, b):.
Solution
  • T — A function can return several values separated by commas: return a, b.
  • F — A function runs as soon as Python reads its def.
  • T — A function can call another function.
  • F — Parameters with a default value can come before those without one: def f(a=1, b):.

return a, b returns a tuple. The def only defines: the body runs when it is called. And parameters with default values always go last; def f(a=1, b): is a syntax error.

Question 8Short answer

What does this program print? Write the exact output.

def minmax(a, b):
    if a < b:
        return a, b
    return b, a

smallest, largest = minmax(8, 3)
print(largest - smallest)
Solution

Answer: 5

With 8 and 3, a < b is false and it returns (3, 8). When unpacking, smallest = 3 and largest = 8: 8 − 3 = 5.

Question 9Single choice

What is the advantage of splitting a program into functions?

  1. The code can be reused and each part tested separately
  2. The program always runs faster
  3. Variables are no longer needed
  4. Python requires it: nothing runs without functions
Solution

Answer: The code can be reused and each part tested separately

It is the idea behind structured programming: splitting a big problem into small pieces with a clear name. If a piece fails, you know where to look.

Question 10Fill in the gaps

Complete the function so that it returns the average of two numbers:

____ average(a, b): and inside ____ (a + b) / 2

Words:

  • function
  • print
  • input
  • return
  • def
Solution

def average(a, b): and inside return (a + b) / 2

Functions are defined with def. If you used print instead of return, the average would appear on screen, but m = average(4, 6) would store None.

Question 11Single choice

What does this code print?

def g(x):
    return x * 2

def h(x):
    return g(x) + 1

print(h(g(3)))
  1. 13
  2. 7
  3. 12
  4. 14
Solution

Answer: 13

It is evaluated from the inside out: first g(3) = 6, and then h(6), which works out g(6) + 1 = 12 + 1 = 13.

Question 12Single choice

What does max(3, 9, 4) return?

  1. 9
  2. 3
  3. (3, 9, 4)
  4. An error: max only accepts lists
Solution

Answer: 9

Python comes with built-in functions such as print, len, max, min, abs or round. They are used just like the ones you define.

Question 13Single choice

What does this code print?

def count(n):
    c = 0
    while n > 0:
        n = n // 10
        c += 1
    return c

print(count(2026))
  1. 4
  2. 2026
  3. 3
  4. 0
Solution

Answer: 4

Each // 10 removes the last digit. The loop does one turn per digit, so the function counts how many the number has: 4.

Visualise it with Python Tutor

Python Tutor is the best way to watch snippet 3’s bug live. Choose Python 3, paste this and step through with Next:

def has_double_wrong(rolls):
    for d1, d2 in rolls:
        if d1 == d2:
            return True
        else:
            return False


def has_double_right(rolls):
    for d1, d2 in rolls:
        if d1 == d2:
            return True
    return False


rolls = [(6, 5), (1, 5), (6, 6)]
print(has_double_wrong(rolls))
print(has_double_right(rolls))

Expected output:

False
True

What to watch step by step:

  1. When entering has_double_wrong, the function’s box receives rolls as an arrow pointing to the same list in the Global frame: it isn’t copied.
  2. On the first pass, d1 = 6 and d2 = 5. The arrow jumps to the else and, on the next step, Return value: False appears. The box disappears with the loop half done: the other two rolls were never checked.
  3. In has_double_right, the first two passes do nothing (the condition is false and there’s no else) and the arrow goes back to the for.
  4. On the third pass, d1 = 6 and d2 = 6: Return value: True. The return False line at the end never runs in this game.

Cheat sheet: Python functions

The essentials in 30 seconds: a function is defined with def name(parameters): and runs when you call it with name(arguments). return sends a value back and ends the function; without return, it returns None. print shows, return hands over. Parameters and variables inside are local: they’re born with the call and die when it ends. In through parameters, out through return.

Defining and calling:

I want to…CodeResult
Define a functiondef charge(total, payment):runs nothing yet
Document it"""Returns the change.""" on the first lineread with charge.__doc__
Call itcharge(3.70, 5.00)1.3
Store what it returnschange = charge(3.70, 5.00)change is 1.3
Pass arguments by namecharge(payment=5.00, total=3.70)1.3
An optional parameterdef charge(total, payment, tip=0):charge(3.70, 5.00) uses tip = 0
Annotate typesdef f(price: float) -> float:informative, not checked

Returning:

I want to…CodeResult
Return a valuereturn round(payment - total, 2)the call is worth that number
Return two valuesreturn euros, centsa tuple: e, c = f(...)
Leave with no valuereturnreturns None
Signal there’s no resultreturn None and if r is None:the caller decides the message
Leave two loopsreturn inside the inner loopleaves the whole function
Search and say “not there”return inside; return None after the forthe tray search

Exercise templates:

# Conversion: receive, calculate and RETURN (exercise 1)
def spanish_to_us(grade):
    return round(grade * 10, 1)

# The data goes in as a parameter and comes out through return (exercise 3)
balance = deposit(balance, 500)

# Highest with its position, starting from an impossible value (exercises 2 and 4)
def highest_and_position(sums):
    best, position, i = 0, 0, 0
    for s in sums:
        i += 1
        if s > best:
            best, position = s, i
    return best, position

# Search: return inside if found, after the loop if not (snippet 3)
def has_double(rolls):
    for d1, d2 in rolls:
        if d1 == d2:
            return True
    return False

Symptom and probable cause:

SymptomProbable cause
TypeError: … 'NoneType' …a function with no return (or with print instead)
The variable is None but the number appeared on screenprint where return was needed
NameError: name 'x' is not defined outside the functionx was local: return it with return
UnboundLocalError: cannot access local variableyou assign an outer variable inside: receive it and return it
TypeError: f() missing 1 required positional argumentan argument is missing in the call
SyntaxError: parameter without a default follows…an optional parameter before a required one
A result with the wrong sign or absurdarguments in the wrong order: use names
A search says “no” although the data is therereturn False inside the loop (in an else)
The function “does nothing”you called it without brackets, or didn’t call it

Before you hand it in, ask yourself:

  1. Does every calculating function have a return, and do only the presentation ones print?
  2. Do I store what functions return (balance = deposit(balance, 500)) instead of calling them on their own?
  3. Do my searches put the “not there” after the loop and not inside it?
  4. Is there a global variable that a function changes? If so, can I pass it in and return it?

If you’ve got this far with the four exercises solved, you have more than enough functions for the next step: keeping them in files of their own and using them from any program with Python modules.

Keep goingAll Python articles

Did this article help you? Here is how you can support SergioLearns →

Sigue aprendiendo

Leave a Reply

Your email address will not be published. Required fields are marked *