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.
Table of Contents
How to use this article
- Read the brief and the expected output. Your program must produce exactly that output (in the two random ones, thanks to a fixed seed).
- 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.
- Before looking at the solutions, do the “What’s wrong here?” section: predict what comes out and check.
- 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, andNoneif 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:
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 >= 9is false,7.5 >= 7is true →"B". Since theelifs are checked top to bottom, each one only needs the lower limit. If you wrote them bottom up (grade >= 3first), 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 finalreturn None.- Plain ASCII names. In the Spanish version of this article the functions are called
espanola_…and notespañ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_linereturns the text.generate_reportprints 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_letterand that’s it. - Impossible values to start with.
grade_max = -1guarantees 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 subjectsunpacks each pair into two variables, just likeeuros, cents = …in the theory.
Trace of the highest and lowest:
| Pass | Subject | grade | total_sum | grade_max (name_max) | grade_min (name_min) |
|---|---|---|---|---|---|
| start | — | — | 0 | −1 | 11 |
| 1 | FP1 | 8.5 | 8.5 | 8.5 (FP1) | 8.5 (FP1) |
| 2 | Statistics | 7.0 | 15.5 | 8.5 (FP1) | 7.0 (Statistics) |
| 3 | Maths | 6.5 | 22.0 | 8.5 (FP1) | 6.5 (Maths) |
| 4 | Databases | 9.0 | 31.0 | 9.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.
recordcompares 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
recordchanges withappend. 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 thereturnand 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:
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_diereturns, it doesn’t print. If it printed,d1 + d2would beNone + None.roll_two_dicereturns a tuple that is unpacked intod1, d2. One function, two values.highest_and_positionandlowest_and_positionreturn 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 theif n > 0skips 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:
| Roll | Dice | Sum | Double? | Highest so far | Lowest so far |
|---|---|---|---|---|---|
| 1 | 6 + 5 | 11 | no | 11 (1) | 11 (1) |
| 2 | 1 + 5 | 6 | no | 11 (1) | 6 (2) |
| 3 | 6 + 6 | 12 | yes | 12 (3) | 6 (2) |
| 4 | 6 + 5 | 11 | no | 12 (3) | 6 (2) |
| 5 | 1 + 6 | 7 | no | 12 (3) | 6 (2) |
| 6 | 3 + 4 | 7 | no | 12 (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.
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.
What does this code print?
def greet(name):
print("Hello", name)
r = greet("Luis")
print(r)Hello Luisand thenNoneHello Luistwice- Only
Hello Luis - 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.
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
baseandheight→ Formal parameters4and3→ Arguments (actual parameters)area→ Name of the functionarea(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.
What is it called when you pass the arguments like this: area(height=3, base=4)?
- Passing by name (keyword arguments)
- Passing by position
- Default values
- 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.
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.
What does this code print?
x = 5
def change():
x = 99
return x
change()
print(x)599- An error, because
xalready exists 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.
About functions:
- A function can return several values separated by commas:
return a, b. - A function runs as soon as Python reads its
def. - A function can call another function.
- 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.
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.
What is the advantage of splitting a program into functions?
- The code can be reused and each part tested separately
- The program always runs faster
- Variables are no longer needed
- 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.
Complete the function so that it returns the average of two numbers:
____ average(a, b): and inside ____ (a + b) / 2
Words:
- function
- 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.
What does this code print?
def g(x):
return x * 2
def h(x):
return g(x) + 1
print(h(g(3)))1371214
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.
What does max(3, 9, 4) return?
93(3, 9, 4)- An error:
maxonly 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.
What does this code print?
def count(n):
c = 0
while n > 0:
n = n // 10
c += 1
return c
print(count(2026))4202630
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:
- When entering
has_double_wrong, the function’s box receivesrollsas an arrow pointing to the same list in the Global frame: it isn’t copied. - On the first pass,
d1 = 6andd2 = 5. The arrow jumps to theelseand, on the next step, Return value: False appears. The box disappears with the loop half done: the other two rolls were never checked. - In
has_double_right, the first two passes do nothing (the condition is false and there’s noelse) and the arrow goes back to thefor. - On the third pass,
d1 = 6andd2 = 6: Return value: True. Thereturn Falseline 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… | Code | Result |
|---|---|---|
| Define a function | def charge(total, payment): | runs nothing yet |
| Document it | """Returns the change.""" on the first line | read with charge.__doc__ |
| Call it | charge(3.70, 5.00) | 1.3 |
| Store what it returns | change = charge(3.70, 5.00) | change is 1.3 |
| Pass arguments by name | charge(payment=5.00, total=3.70) | 1.3 |
| An optional parameter | def charge(total, payment, tip=0): | charge(3.70, 5.00) uses tip = 0 |
| Annotate types | def f(price: float) -> float: | informative, not checked |
Returning:
| I want to… | Code | Result |
|---|---|---|
| Return a value | return round(payment - total, 2) | the call is worth that number |
| Return two values | return euros, cents | a tuple: e, c = f(...) |
| Leave with no value | return | returns None |
| Signal there’s no result | return None and if r is None: | the caller decides the message |
| Leave two loops | return inside the inner loop | leaves the whole function |
| Search and say “not there” | return inside; return None after the for | the 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:
| Symptom | Probable cause |
|---|---|
TypeError: … 'NoneType' … | a function with no return (or with print instead) |
The variable is None but the number appeared on screen | print where return was needed |
NameError: name 'x' is not defined outside the function | x was local: return it with return |
UnboundLocalError: cannot access local variable | you assign an outer variable inside: receive it and return it |
TypeError: f() missing 1 required positional argument | an 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 absurd | arguments in the wrong order: use names |
| A search says “no” although the data is there | return 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:
- Does every calculating function have a
return, and do only the presentation onesprint? - Do I store what functions return (
balance = deposit(balance, 500)) instead of calling them on their own? - Do my searches put the “not there” after the loop and not inside it?
- 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 articlesDid this article help you? Here is how you can support SergioLearns →




The hardware that holds up the operating system — theory with my IC2 assignment on the printer, an interrupt that stops everything and a cache that hits (and the infinite loop that didn’t freeze my laptop)