My function printed the total, but gave me None
See the problem
Section titled “See the problem”Your shop script adds up a basket and takes off a discount. You need it in three places, so you copy the five lines three times. Then the discount rule changes. You fix two copies and miss the third.
So you put the lines in a function, basket_total. It prints the total, and it looks right:
>>> basket_total([10.00, 10.00])Basket: 20.00Now you need the total with tax: basket_total([10.00, 10.00]) * 1.2. Python stops:
TypeError: unsupported operand type(s) for *: 'NoneType' and 'float'The function showed 20.00. Why did it give back None?
Define the task
Section titled “Define the task”- Input: a list of prices, and a discount in percent. The discount is 0 when left out.
- Output: the total after the discount, as a number, rounded to 2 places. The caller decides whether to print it, add tax, or compare it.
- The hard part: showing a value on the screen isn’t the same as handing it back.
Example: basket_total([2.50, 1.50], discount=10) gives 3.6, and basket_total([10, 10]) * 1.2 gives 24.0.
Pause and think
Section titled “Pause and think”Hints open one at a time:
Hint 1 of 3
A function hands back whatever follows return. With no return, it hands back None.
Hint 2 of 3
The screen is for people. return is for code.
Hint 3 of 3
Do the sum in the function and return it. Print it outside, where you call the function.
Understand the idea
Section titled “Understand the idea”def makes a function. It names some steps, so you can run them again with one line.
def basket_total(prices, discount=0): # two parameters; discount has a default ... return total # hands total back to the caller- Parameters are the names in brackets. Each call gives them values, called arguments.
- A default like
discount=0is used when the caller leaves that argument out. - Call by position or by name:
basket_total([10], 50)andbasket_total([10], discount=50)do the same. returnends the function and hands one value back. A function that ends withoutreturnhands backNone.
Here are two functions side by side: one returns, one prints. Each call gets its own box. The player stops to ask what happens next. Then type your own lines under it.
Built-inPython’s own names
- len
- max
Globalrunning now
no names yet
Step 1 of 11. Before the first line runs, the global box is empty. The built-in box holds names like print and len.
def total(a, b): return a + b def show( a, b): print( a + b) x = total( 2, 3) y = show( 2, 3) print( x, y)
x gets 5 from return. y gets None, though show put 5 on the screen.
Climb the ladder
Section titled “Climb the ladder”All three add the prices once, so all take O(n) time for n prices. What changes is how many places hold the rule, and whether the answer comes back.
Copy the lines
The idea: Paste the same loop and sum wherever you need a total.
On the two baskets: each copy adds its prices, takes off its discount, and prints. Both totals are right.
# The same lines, copied to each place that needs a basket total.prices = [2.50, 1.50]total = 0for price in prices: total += pricetotal = total * (1 - 10 / 100)print(f"Basket: {total:.2f}")
prices = [10.00, 10.00]total = 0for price in prices: total += pricetotal = total * (1 - 0 / 100)print(f"Basket: {total:.2f}")- Time
O(n)- Space
O(1)- Why
- One pass over the prices, in each copy.
Where it breaks: every copy holds the rule. Change the rule, and you must find and fix every copy. Miss one, and two parts of the shop give different totals.
A function that prints
The idea: Put the lines in def basket_total(prices, discount=0) and print the total at the end.
The waste in Okay: the same five lines live in many places.
Now the rule lives in one place, and each call is one line.
Highlighted lines are new in this rung.
def basket_total(prices: list[float], discount: float = 0) -> None: total = 0 for price in prices: total += price total = total * (1 - discount / 100) print(f"Basket: {total:.2f}")- Time
O(n)- Space
O(1)- Why
- One pass over the prices.
What changed from the previous rung
# The same lines, copied to each place that needs a basket total.prices = [2.50, 1.50]total = 0for price in prices: total += pricetotal = total * (1 - 10 / 100)print(f"Basket: {total:.2f}")
prices = [10.00, 10.00]total = 0for price in prices: total += pricetotal = total * (1 - 0 / 100)print(f"Basket: {total:.2f}")def basket_total(prices: list[float], discount: float = 0) -> None: total = 0 for price in prices: total += price total = total * (1 - discount / 100) print(f"Basket: {total:.2f}")Where it breaks: the function ends without return, so it hands back None. You can see the total, but your code can’t use it: basket_total(...) * 1.2 is a TypeError.
A function that returns
The idea: Return the rounded total, and let the caller print it.
The waste in Better: it decides for every caller that the total should be printed.
main() shows two callers: one prints the total, one adds tax first. round(…, 2) keeps money to two places.
Highlighted lines are new in this rung.
def basket_total(prices: list[float], discount: float = 0) -> float: total = 0 for price in prices: total += price return round(total * (1 - discount / 100), 2)
def main() -> None: print(f"Basket: {basket_total([2.50, 1.50], discount=10):.2f}") with_tax = basket_total([10.00, 10.00]) * 1.2 print(f"With tax: {with_tax:.2f}")- Time
O(n)- Space
O(1)- Why
- One pass over the prices.
What changed from the previous rung
def basket_total(prices: list[float], discount: float = 0) -> None:def basket_total(prices: list[float], discount: float = 0) -> float: total = 0 for price in prices: total += price total = total * (1 - discount / 100) print(f"Basket: {total:.2f}") return round(total * (1 - discount / 100), 2)
def main() -> None: print(f"Basket: {basket_total([2.50, 1.50], discount=10):.2f}") with_tax = basket_total([10.00, 10.00]) * 1.2 print(f"With tax: {with_tax:.2f}")When Best isn’t best: Python already has sum(prices). In real code, write round(sum(prices) * (1 - discount / 100), 2). The loop here shows what sum() does.
Write it yourself
Section titled “Write it yourself”Now write basket_total in the page. Press Run tests to check your code. Python runs in your browser, so nothing is sent anywhere.
Try Better from the ladder first. It prints every total, and still fails every test.
The basket total
Ctrl + Enter runs- Not run yet:
basket_total([2.5, 1.5])→4 - Not run yet:
basket_total([2.5, 1.5], discount=10)→3.6 - Not run yet:
basket_total([10, 10], 10)→18 - Not run yet: Extra test
- Not run yet: Extra test
Extra tests check tricky cases. You see their input only if they fail.
Try your own input
Compare
Section titled “Compare”| Way | Where the rule lives | Can code use the total? | Use it when |
|---|---|---|---|
| Okay: copies | In every copy | Yes, in that copy | Never more than once |
| Better: prints | In one function | No: it gets None | Never for a value someone needs |
| Best: returns | In one function | Yes | Almost always |
Common mistakes and edge cases
Section titled “Common mistakes and edge cases”- Code after
returnnever runs:returnends the function at once. - Calling without brackets:
basket_totalis the function itself;basket_total([1])calls it. - Changing a name inside the function doesn’t change a name of the same name outside. The scope lesson explains why.
- Too many arguments, or too few: Python raises
TypeError: basket_total() missing 1 required positional argument: 'prices'. - Defaults must be last:
def f(a=1, b)is aSyntaxError.
Say it in the interview
Section titled “Say it in the interview”Questions to ask first
- Should the function show the result, or return it for other code?
- What should an empty input give?
A 60-second walkthrough
“I put the logic in one function so the rule lives in one place. It takes the prices and an optional discount, defaulting to 0. It returns the rounded total rather than printing it, so callers can print it, add tax or test it. A function without
returngives backNone, which is why the printing version failed when we multiplied its result.”
Likely follow-ups
- What does a function return if there’s no
return?None. - Can a function return more than one value? It returns one object, but that can be a tuple:
return low, high, thenlow, high = f(). - Parameter or argument? The parameter is the name in the
def; the argument is the value passed in a call.
Practice
Section titled “Practice”Three problems, from easy to hard. Each has hints, tests you can run here, and its own Okay, Better and Best answers. Try before you open them.
1. The average (easy)
Section titled “1. The average (easy)”Write average(nums). Return the average of a list of numbers. With no numbers there is no average, so return None.
Hint 1 of 3
The average is sum(nums) / len(nums).
Hint 2 of 3
What does dividing by len([]) do?
Hint 3 of 3
Is 0 a good answer for “no numbers”? What is the average of [-1, 1]?
The average
Ctrl + Enter runs- Not run yet:
average([2, 4])→3 - Not run yet:
average([5])→5 - Not run yet:
average([])→None - Not run yet: Extra test
Extra tests check tricky cases. You see their input only if they fail.
Try your own input
Show the Okay, Better and Best answers
Sum over length
The idea: Return sum(nums) / len(nums).
[2, 4] gives 3.0.
def average(nums: list[float]) -> float: return sum(nums) / len(nums)- Time
O(n)- Space
O(1)- Why
- sum() looks at each number once.
Where it breaks: an empty list divides by zero, and Python raises ZeroDivisionError.
0 for an empty list
The idea: Return 0 when the list is empty.
The waste in Okay: it crashes on a list that is easy to get, like a day with no sales.
Highlighted lines are new in this rung.
def average(nums: list[float]) -> float: if len(nums) == 0: return 0 return sum(nums) / len(nums)- Time
O(n)- Space
O(1)- Why
- One check, then sum().
What changed from the previous rung
def average(nums: list[float]) -> float: if len(nums) == 0: return 0 return sum(nums) / len(nums)Where it breaks: 0 is a real average: [-1, 1] gives 0 too. The caller can’t tell “no numbers” from “numbers that average 0”.
None for no average
The idea: Return None when there are no numbers.
The waste in Better: it uses a real number to mean “nothing”.
not nums is true for an empty list. The type hint float | None warns callers to check.
Highlighted lines are new in this rung.
def average(nums: list[float]) -> float | None: if not nums: return None # no numbers: there is no average return sum(nums) / len(nums)- Time
O(n)- Space
O(1)- Why
- One check, then sum().
What changed from the previous rung
def average(nums: list[float]) -> float: if len(nums) == 0: return 0def average(nums: list[float]) -> float | None: if not nums: return None # no numbers: there is no average return sum(nums) / len(nums)When Best isn’t best: Python’s statistics.mean() raises StatisticsError on an empty list. Raising is better when an empty list means a bug, not a normal day.
2. Count the vowels (medium)
Section titled “2. Count the vowels (medium)”Write count_vowels(text). Count the letters a, e, i, o and u, small or capital.
Example: count_vowels("Apple") gives 2.
Hint 1 of 3
Loop over the text: for ch in text gives one character at a time.
Hint 2 of 3
ch in "aeiou" is true when ch is one of those letters.
Hint 3 of 3
A small helper, is_vowel(ch), can lower the case first.
Count the vowels
Ctrl + Enter runs- Not run yet:
count_vowels('banana')→3 - Not run yet:
count_vowels('Apple')→2 - Not run yet: Extra test
- Not run yet: Extra test
Extra tests check tricky cases. You see their input only if they fail.
Try your own input
Show the Okay, Better and Best answers
Five tests joined with or
The idea: Compare each character with each vowel.
"banana" gives 3.
def count_vowels(text: str) -> int: count = 0 for ch in text: if ch == "a" or ch == "e" or ch == "i" or ch == "o" or ch == "u": count += 1 return count- Time
O(n)- Space
O(1)- Why
- One look at each character.
Where it breaks: "A" matches none of the tests, so "Apple" gives 1. And the long line is easy to get wrong.
ch in “aeiou”
The idea: One test: is the character in the text “aeiou”?
The waste in Okay: five comparisons where one in does.
Highlighted lines are new in this rung.
def count_vowels(text: str) -> int: count = 0 for ch in text: if ch in "aeiou": count += 1 return count- Time
O(n)- Space
O(1)- Why
- One look at each character; “aeiou” is always five letters.
What changed from the previous rung
def count_vowels(text: str) -> int: count = 0 for ch in text: if ch == "a" or ch == "e" or ch == "i" or ch == "o" or ch == "u": if ch in "aeiou": count += 1 return countWhere it breaks: capitals still aren’t counted.
A helper that lowers the case
The idea: is_vowel(ch) checks ch.lower() in “aeiou”, and count_vowels calls it.
The waste in Better: it treats "A" and "a" as different letters.
The helper has a name that says what it checks, and can be tested on its own.
Highlighted lines are new in this rung.
def is_vowel(ch: str) -> bool: return ch.lower() in "aeiou"
def count_vowels(text: str) -> int: count = 0 for ch in text: if is_vowel(ch): count += 1 return count- Time
O(n)- Space
O(1)- Why
- One look at each character.
What changed from the previous rung
def is_vowel(ch: str) -> bool: return ch.lower() in "aeiou"
def count_vowels(text: str) -> int: count = 0 for ch in text: if ch in "aeiou": if is_vowel(ch): count += 1 return countWhen Best isn’t best: for letters like é or ö, decide what counts as a vowel first. And sum(1 for ch in text if is_vowel(ch)) does the counting in one line, once you know the loop.
3. What’s wrong with the password (hard)
Section titled “3. What’s wrong with the password (hard)”Write problems(password). Return a list of everything the password is missing, in this order: "at least 8 characters", "a digit", "a capital letter". A good password gives [].
Example: problems("abc") gives all three.
Hint 1 of 3
Returning at the first problem tells the user one thing at a time. Collect them all in a list.
Hint 2 of 3
ch.isdigit() and ch.isupper() test one character.
Hint 3 of 3
Small helpers like has_digit(password) can return True as soon as they find one.
What’s wrong with the password
Ctrl + Enter runs- Not run yet:
problems('Abcdefg1')→[] - Not run yet:
problems('abcdefgh')→['a digit', 'a capital letter'] - Not run yet:
problems('abc')→['at least 8 characters', 'a digit', 'a capital letter'] - Not run yet: Extra test
- Not run yet: Extra test
Extra tests check tricky cases. You see their input only if they fail.
Try your own input
Show the Okay, Better and Best answers
Return at the first problem
The idea: Check each rule in turn and return as soon as one fails.
A good password gives [], and each rule is checked.
def problems(password: str) -> list[str]: if len(password) < 8: return ["at least 8 characters"] for ch in password: if ch.isdigit(): break else: return ["a digit"] for ch in password: if ch.isupper(): break else: return ["a capital letter"] return []- Time
O(n)- Space
O(1)- Why
- At most two passes over the n characters.
Where it breaks: the user learns one problem, fixes it, and is told the next. "abc" gives only ["at least 8 characters"].
Collect every problem
The idea: Append each failed rule to a list, using a flag for each character test.
The waste in Okay: it stops at the first problem when the user needs all of them.
This passes every test. But the two loops are almost the same, and each keeps going after it finds what it was looking for.
Highlighted lines are new in this rung.
def problems(password: str) -> list[str]: found = [] if len(password) < 8: found.append("at least 8 characters") has_digit = False for ch in password: if ch.isdigit(): has_digit = True if not has_digit: found.append("a digit") has_capital = False for ch in password: if ch.isupper(): has_capital = True if not has_capital: found.append("a capital letter") return found- Time
O(n)- Space
O(1)- Why
- Two full passes over the characters.
What changed from the previous rung
def problems(password: str) -> list[str]: found = [] if len(password) < 8: return ["at least 8 characters"] found.append("at least 8 characters") has_digit = False for ch in password: if ch.isdigit(): break else: return ["a digit"] has_digit = True if not has_digit: found.append("a digit") has_capital = False for ch in password: if ch.isupper(): break else: return ["a capital letter"] return [] has_capital = True if not has_capital: found.append("a capital letter") return foundSmall helpers
The idea: has_digit() and has_capital() each return True at the first match; problems() reads like the rules.
The waste in Better: repeated loops with flags, inside one long function.
Each helper does one job and has a name that says what it checks. problems() now reads like the list of rules.
Highlighted lines are new in this rung.
def has_digit(password: str) -> bool: for ch in password: # noqa: SIM110 (the loop any() would run) if ch.isdigit(): return True return False
def has_capital(password: str) -> bool: for ch in password: # noqa: SIM110 (the loop any() would run) if ch.isupper(): return True return False
def problems(password: str) -> list[str]: found = [] if len(password) < 8: found.append("at least 8 characters") if not has_digit(password): found.append("a digit") if not has_capital(password): found.append("a capital letter") return found- Time
O(n)- Space
O(1)- Why
- Each helper stops at its first match.
What changed from the previous rung
def has_digit(password: str) -> bool: for ch in password: # noqa: SIM110 (the loop any() would run) if ch.isdigit(): return True return False
def has_capital(password: str) -> bool: for ch in password: # noqa: SIM110 (the loop any() would run) if ch.isupper(): return True return False
def problems(password: str) -> list[str]: found = [] if len(password) < 8: found.append("at least 8 characters") has_digit = False for ch in password: if ch.isdigit(): has_digit = True if not has_digit: if not has_digit(password): found.append("a digit") has_capital = False for ch in password: if ch.isupper(): has_capital = True if not has_capital: if not has_capital(password): found.append("a capital letter") return foundWhen Best isn’t best: Python’s any() writes each helper in one line: any(ch.isdigit() for ch in password). And real sites check length and known leaked passwords, not just character types.
Check yourself
Section titled “Check yourself”A few quick questions. Ones you miss are remembered, so you can find your weak spots later.
Flashcards
Section titled “Flashcards”Add these to your review deck. Each card comes back a little later every time you get it right.
Cheat sheet
Section titled “Cheat sheet”def name(param, other=default):makes a function; defaults come last.return valueends the function and handsvalueback. NoreturnmeansNone.- Print for people, return for code. Do the work in the function and print outside it.
- Call by position
f(1, 2)or by namef(a=1, b=2). - Small functions with clear names are easier to read and to test.
Go deeper
Section titled “Go deeper”- Defining functions, in the Python tutorial.
- Next: Functions,
*args,**kwargsand the mutable-default trap, when you know lists and dicts.
Can you do these now?
If you’re not sure of any of these, scroll back to that part before moving on.
Status: Not started