Session 9 of the Python Fundamentals series, covered in depth. It packages code into a named, reusable tool, covering def, parameters as inputs, return as output, and calling with parentheses. It draws the difference between return and print, shows that a function with no return gives None, and covers default arguments, functions that call other functions, and refactoring a repeated block into a single function. The traps are printing when you needed to return, calling without parentheses, and calling before the def. Every snippet and error message was copied verbatim from CPython 3.12.
Subject: Python Fundamentals · 95 slides · code lesson
Open the interactive version of this deck · Homework for this lesson
Title
Python Fundamentals - Session 9
Name a block of code once, use it again and again
Objectives
So far each program is one long script. Functions let you name and reuse steps. By the end you can:
def, give it parameters, and call it.return and print.return gives back None.Warm-up
Discussion prompt
Before we open Session 9 - Functions: without looking back, what was the main idea of Session 8 - Dictionaries, and what could you do by the end of it that you could not do before?
Hint: One sentence for the idea, one for the skill. If the second one is blank, that is the part to revisit.
Answer:
That session covers key-value lookup, d[key] against d.get(), adding and updating, the in operator, looping over items, replacing an if/elif chain with a lookup, and tallying counts.
Section
Part 1
Concept
A function bundles a few steps under one name. Call the name to run those steps - as many times as you like.
function — A named block of code you can call. It may take inputs (parameters) and hand back a result (return).
Counterexample
Discussion prompt
A function bundles a few steps under one name. Call the name to run those steps - as many times as you like.
That is stated as though it always holds. Do one of two things: produce a case where it fails, or say precisely what rules such a case out. "It just does" is not on the menu.
Hint: Hunt at the extremes first — zero, one, negative, empty, equal. If every extreme survives, the reason they survive is the proof.
Concept
Start with def, the name, parentheses for inputs, and a colon. The indented block below is the function's body.
Defining a function does not run it - it just teaches Python the recipe for later.
Ranking
Put in order
Put the moves of Define and call greet into the order they have to happen.
Why: These are the moves of the worked example in the order it makes them, and each one is set up by the one before it. Python just records the recipe: given a name, build a greeting.
Worked example
def greet(name):
return "Hello, " + name + "!"
print(greet("Sam"))Lines 1-2 define greet - nothing runs yet
Why: Python just records the recipe: given a name, build a greeting.
Line 4 calls it with "Sam"
Why: Now the body runs with name = "Sam" and hands back the greeting.
Read the output
Why: Verified by execution: Hello, Sam!
| call | name | returns |
|---|---|---|
| greet("Sam") | Sam | Hello, Sam! |
Error analysis
Annotate
Walk the callouts on Define and call greet. Each one is a place this is easy to get subtly wrong.
Concept
greet("Sam") - the parentheses mean 'run it now, with this input'. Without them you have only the name, not the result.
Analogy
Discussion prompt
Explain Parentheses call the function by analogy to something with no Python Fundamentals in it at all — a queue, a recipe, a map, a bank balance, whatever fits. Then say where your analogy breaks.
Hint: An analogy that never breaks is not an analogy, it is the same idea wearing a hat. Find the seam — that is the part that is actually new.
Answer:
greet("Sam") - the parentheses mean 'run it now, with this input'. Without them you have only the name, not the result.
Intuition
Picture a machine with an input slot and an output slot. You drop a value in (the argument), it does its work, and a result comes out (the return).
You do not care how the machine works inside when you use it - you just feed it inputs and take the output. That is the power of naming a block.
Explain it
Discussion prompt
Explain A function is a little machine to a student a year behind you. No notation, no jargon they have not met — and it still has to be true.
Hint: If your explanation needs a symbol they have never seen, you are describing the notation rather than the idea.
Answer:
Picture a machine with an input slot and an output slot. You drop a value in (the argument), it does its work, and a result comes out (the return).
Concept
Code inside a function sits idle until the function is called. Each call runs the body fresh, with the inputs you gave.
Call it three times and the body runs three times - that is reuse.
Section
Part 2
Concept
The names inside the def parentheses are parameters - placeholders for the values you will pass in.
parameter vs argument — A parameter is the name in the definition (def add(a, b)). An argument is the actual value you pass when calling (add(3, 4)).
Definition probe
Sort into buckets
Every line below is part of the definition of function or of parameter vs argument — one or the other, never both. Put each where it belongs.
Worked example
def add(a, b):
return a + b
print(add(3, 4))a and b are filled by the call
Why: add(3, 4) sets a = 3 and b = 4.
The body computes and returns
Why: Verified by execution: 3 + 4 = 7, so it prints 7.
| a | b | returns |
|---|---|---|
| 3 | 4 | 7 |
Blank canvas
Draw it
Draw what Two inputs: add just did — the shape of it, not the line-by-line working. One picture, labels only where you need them. Then check it against the steps: anything you could not draw is a step you followed rather than understood.
Concept
The first argument goes to the first parameter, the second to the second, and so on. Order matters.
Worked example
def subtract(a, b):
return a - b
print(subtract(10, 3))
print(subtract(3, 10))Swapping the arguments swaps a and b
Why: First call: a=10, b=3. Second: a=3, b=10.
Read the output
Why: Verified by execution: 7 then -7. Same function, different order in.
| call | a | b | returns |
|---|---|---|---|
| subtract(10, 3) | 10 | 3 | 7 |
| subtract(3, 10) | 3 | 10 | -7 |
Comparison
Comparison matrix
From Order changes the result: refill the returns column from what you know. The rest of the table is as it appeared.
| call | a | b | returns |
|---|---|---|---|
| subtract(10, 3) | 10 | 3 | 7 |
| subtract(3, 10) | 3 | 10 | -7 |
Worked example
def area(w, h):
return w * h
print(area(3, 4))
print(area(5, 2))One definition, many uses
Why: Each call reuses the same recipe with new numbers.
Read the output
Why: Verified by execution: 12 then 10.
| call | returns |
|---|---|
| area(3, 4) | 12 |
| area(5, 2) | 10 |
Trade off
Comparison matrix
From A reusable area function: every row here is a choice with a cost. Fill the returns column, then say which row you would actually pick and what you give up for it.
| call | returns |
|---|---|
| area(3, 4) | 12 |
| area(5, 2) | 10 |
Section
Part 3
Concept
return value sends a result back to whoever called the function. The call then 'becomes' that value.
So add(3, 4) can be stored, printed, or used in more math - it stands in for 7.
Pattern
Predict first
The table runs: add(3, 4) | 7 · total | 7
In Store the returned value, given the rows so far: what is the next one — the row where step is total + 1?
Correct: total + 1 | 8
| step | value |
|---|---|
| add(3, 4) | 7 |
| total | 7 |
| total + 1 | 8 |
Why: The relationship between the columns, not the individual numbers, is what generates the next row. Verified by execution: total + 1 is 8.
Worked example
def add(a, b):
return a + b
total = add(3, 4)
print(total + 1)The call's result is stored in total
Why: add(3, 4) returns 7, so total is 7.
The value can be used further
Why: Verified by execution: total + 1 is 8. A returned value is a real value.
| step | value |
|---|---|
| add(3, 4) | 7 |
| total | 7 |
| total + 1 | 8 |
Pattern
Step through it
Step through Store the returned value one row at a time. What is driving the change, and what would the row after the last one be?
Concept
The moment a return runs, the function stops and hands back that value. Any lines after it in the same path do not run.
This lets you return early - answer as soon as you know the result.
Sorting
Sort into buckets
These are the pieces of Session 9 - Functions, out of order. Put each one back under the part of the lesson it belongs to.
Worked example
def sign(n):
if n > 0:
return "positive"
if n < 0:
return "negative"
return "zero"
print(sign(-4))The first matching return wins
Why: n < 0 is true, so it returns negative and stops - the zero line is never reached.
Read the output
Why: Verified by execution: negative. No else needed - a return already exits.
| n | returns |
|---|---|
| 7 | positive |
| -4 | negative |
| 0 | zero |
Pattern
Step through it
Step through Return early one row at a time. What is driving the change, and what would the row after the last one be?
Intuition
Think of the machine's output slot. return is what you place in it - the single value that comes out when the machine finishes.
No return, nothing in the slot: the caller gets None (coming up).
Section
Part 4
Concept
print puts text on the screen for a human. return hands a value back to the program to use.
They feel similar because you often see a result either way - but only a returned value can be reused.
Fill the middle
Fill in the blanks
From Two versions of double — one line has had its right-hand side removed. Put it back.
def double_print(n):
print(n * 2)
def double_return(n):
return n * 2
double_print(6)
r = double_return(6)
print(r + 1)
Why: r is what everything below it consumes, so the wrong expression here fails later and somewhere else. You see 12, but there is no value to store.
Worked example
def double_print(n):
print(n * 2)
def double_return(n):
return n * 2
double_print(6)
r = double_return(6)
print(r + 1)double_print shows 12 but hands nothing back
Why: You see 12, but there is no value to store.
double_return gives 12 to reuse
Why: Verified by execution: r is 12, and r + 1 is 13. Prints 12 then 13.
| version | screen | reusable value |
|---|---|---|
| double_print(6) | 12 | none |
| double_return(6) | - | 12 |
Anomaly
Predict first
A student writes this, and it looks reasonable:
The function prints instead of returning, then you try to reuse it.
It is wrong. Say what breaks — and say it before you turn the page.
Correct: total becomes None, so total * 2 tries None * 2 and crashes.
return the value so the caller can use it.
Why: total becomes None, so total * 2 tries None * 2 and crashes. You saw 7 on screen, but could not use it.
Trap
The function prints instead of returning, then you try to reuse it.
def add(a, b):
print(a + b)
total = add(3, 4)
print(total * 2)add prints but returns None
Why: total becomes None, so total * 2 tries None * 2 and crashes. You saw 7 on screen, but could not use it.
| step | value / result |
|---|---|
| add(3, 4) | prints 7, returns None |
| total * 2 | TypeError: unsupported operand type(s) |
return the value so the caller can use it.
def add(a, b):
return a + b
total = add(3, 4)
print(total * 2)Now total holds 7
Why: return hands 7 back; total * 2 is 14. Real output: 14. Rule of thumb: return the result, and let the caller decide whether to print it.
| step | value |
|---|---|
| total | 7 |
| total * 2 | 14 |
Concept
Return, and the caller chooses what to do: print it, store it, compare it, or feed it into another function.
Print inside the function, and you have locked it into one behavior. Returning keeps functions reusable.
Fill the middle
Fill in the blanks
From Reuse a returned value three ways — one line has had its right-hand side removed. Put it back.
def area(w, h):
return w * h
a = area(3, 4)
print(a)
print(a > 10)
print(a + area(2, 2))
Why: a is what everything below it consumes, so the wrong expression here fails later and somewhere else. Stored, compared, and added to another call - all because area returns.
Worked example
def area(w, h):
return w * h
a = area(3, 4)
print(a)
print(a > 10)
print(a + area(2, 2))The one result is used three ways
Why: Stored, compared, and added to another call - all because area returns.
Read the output
Why: Verified by execution: 12, then True (12 > 10), then 16 (12 + 4).
| expression | value |
|---|---|
| a | 12 |
| a > 10 | True |
| a + area(2, 2) | 16 |
Pattern
Step through it
Step through Reuse a returned value three ways one row at a time. What is driving the change, and what would the row after the last one be?
Section
Part 5
Concept
If a function never runs a return, it still hands something back: the special value None, meaning 'no value'.
None — Python's 'nothing here' value. A function with no return gives back None; storing that result gives you None.
Matching
Match the pairs
Match each term to the definition this lesson gave it — not the one you would guess from the word.
Why: These are the working definitions of function, parameter vs argument, None as Session 9 - Functions uses them. Pairing them correctly is the test of whether you could state each one with the slide switched off.
Fill the middle
Fill in the blanks
From A print-only function returns None — one line has had its right-hand side removed. Put it back.
def show(x):
print("value is", x)
result = show(5)
print(result)
Why: result is what everything below it consumes, so the wrong expression here fails later and somewhere else. Verified by execution: prints value is 5, then None.
Worked example
def show(x):
print("value is", x)
result = show(5)
print(result)show prints but has no return
Why: So its call hands back None.
result is None
Why: Verified by execution: prints value is 5, then None.
| step | output / value |
|---|---|
| show(5) | prints value is 5 |
| result | None |
Comparison
Comparison matrix
From A print-only function returns None: refill the output / value column from what you know. The rest of the table is as it appeared.
| step | output / value |
|---|---|
| show(5) | prints value is 5 |
| result | None |
Concept
Seeing None where you expected a real result almost always means you forgot the return.
Check: does the function actually return the value, or only print it (or compute it and drop it)?
Anomaly
Predict first
A student writes this, and it looks reasonable:
The function computes but never returns.
It is wrong. Say what breaks — and say it before you turn the page.
Correct: Without a return, square hands back None, so print shows None - the 25 is lost inside the function.
Return the computed value.
Why: Without a return, square hands back None, so print shows None - the 25 is lost inside the function.
Trap
The function computes but never returns.
def square(n):
result = n * n
print(square(5))result is computed then thrown away
Why: Without a return, square hands back None, so print shows None - the 25 is lost inside the function.
| you write | prints |
|---|---|
| square(5) | None |
Return the computed value.
def square(n):
return n * n
print(square(5))return sends 25 back
Why: Now the call is 25. Real output: 25. Compute, then return what you computed.
| you write | prints |
|---|---|
| square(5) | 25 |
Section
Part 6
Concept
Give a parameter a default with = in the def. If the caller omits it, the default is used.
def greet(name, greeting="Hi"): lets you call greet("Ana") or greet("Ana", "Welcome").
Worked example
def greet(name, greeting="Hi"):
return greeting + ", " + name
print(greet("Ana"))
print(greet("Ana", "Welcome"))Omit greeting and it defaults to Hi
Why: First call uses the default; second overrides it.
Read the output
Why: Verified by execution: Hi, Ana then Welcome, Ana.
| call | greeting | returns |
|---|---|---|
| greet("Ana") | Hi | Hi, Ana |
| greet("Ana", "Welcome") | Welcome | Welcome, Ana |
Concept
A function's body can call other functions. This is how you build bigger tools from small, tested ones.
Worked example
def is_even(n):
return n % 2 == 0
num = 7
if is_even(num):
print("even")
else:
print("odd")is_even returns a bool the if can use
Why: is_even(7) is False, so the else runs.
Read the output
Why: Verified by execution: prints odd. A returning function slots straight into a condition.
| num | is_even(num) | prints |
|---|---|---|
| 7 | False | odd |
| 4 | True | even |
Concept
If you find yourself copying the same few lines, that is a signal to make a function. Write it once, call it everywhere.
Fix a bug in one place instead of ten. This is the practical heart of why functions matter.
Section
Part 7
Concept
Take a chunk you keep rewriting, wrap it in a def, replace the varying parts with parameters, and return the result.
Worked example
def effective(attack, defender):
if attack == "water" and defender == "fire":
return "super effective"
return "normal"
print(effective("water", "fire"))
print(effective("grass", "fire"))The decision lives in one named place
Why: The two varying inputs become parameters; the answer is returned.
Call it as often as you like
Why: Verified by execution: super effective, then normal.
| call | returns |
|---|---|
| effective("water", "fire") | super effective |
| effective("grass", "fire") | normal |
Blank canvas
Draw it
Draw what A matchup checker as a function just did — the shape of it, not the line-by-line working. One picture, labels only where you need them. Then check it against the steps: anything you could not draw is a step you followed rather than understood.
Pattern
Predict first
The table runs: 3x4 | 12 | 12 · 5x2 | 10 | 22
In Reuse across many calls, given the rows so far: what is the next one — the row where room is 6x6?
Correct: 6x6 | 36 | 58
| room | area | total |
|---|---|---|
| 3x4 | 12 | 12 |
| 5x2 | 10 | 22 |
| 6x6 | 36 | 58 |
Why: The relationship between the columns, not the individual numbers, is what generates the next row. area is reused inside the loop; no repeated w * h everywhere.
Worked example
def area(w, h):
return w * h
rooms = [(3, 4), (5, 2), (6, 6)]
total = 0
for w, h in rooms:
total += area(w, h)
print(total)One function, called once per room
Why: area is reused inside the loop; no repeated w * h everywhere.
Trace the total
Why: Verified by execution: 12 + 10 + 36 = 58.
| room | area | total |
|---|---|---|
| 3x4 | 12 | 12 |
| 5x2 | 10 | 22 |
| 6x6 | 36 | 58 |
Error analysis
Annotate
Walk the callouts on Reuse across many calls. Each one is a place this is easy to get subtly wrong.
Section
Part 8
Anomaly
Predict first
A student writes this, and it looks reasonable:
Forgetting the parentheses on the call.
It is wrong. Say what breaks — and say it before you turn the page.
Correct: You get the function object, not a result - it never runs.
Add parentheses (and the argument) to actually call it.
Why: You get the function object, not a result - it never runs. Nothing is greeted.
Trap
Forgetting the parentheses on the call.
def greet(name):
return "Hi " + name
print(greet)greet without () is the function itself
Why: You get the function object, not a result - it never runs. Nothing is greeted.
| you write | prints |
|---|---|
| print(greet) | <function greet ...> (the function, not a result) |
Add parentheses (and the argument) to actually call it.
def greet(name):
return "Hi " + name
print(greet("Sam"))greet("Sam") runs the function
Why: The parentheses trigger the call. Real output: Hi Sam.
| you write | prints |
|---|---|
| print(greet("Sam")) | Hi Sam |
Two truths and a lie
Sort into buckets
Some of these hold up and some are the exact mistakes this lesson is built to prevent. Sort them.
greet("Sam") - the parentheses mean 'run it now, with this input'. Without them you have only the name, not the result.; Picture a machine with an input slot and an output slot. You drop a value in (the argument), it does its work, and a result comes out (the return).Concept
Python reads top to bottom, so a function must be defined above the line that calls it.
Calling a function that has not been defined yet is a NameError.
Worked example
def say_hi():
print("hi")
say_hi()def comes first, call second
Why: By the time say_hi() runs, Python already knows the recipe.
Reversed order fails
Why: Verified by execution: this prints hi. Putting say_hi() above the def would raise NameError: name 'say_hi' is not defined.
| order | result |
|---|---|
| def then call | hi |
| call then def | NameError |
Cost model
Annotate
In Order of def and call, before reading the notes: mark where the time actually goes. Which line dominates?
Concept
You can name arguments in the call: greet(name="Ana", greeting="Yo"). Named arguments can even come in any order.
This makes a call self-documenting - you can see which value is which without checking the definition.
Explain it
Discussion prompt
Explain Passing arguments by name to a student a year behind you. No notation, no jargon they have not met — and it still has to be true.
Hint: If your explanation needs a symbol they have never seen, you are describing the notation rather than the idea.
Answer:
You can name arguments in the call: greet(name="Ana", greeting="Yo"). Named arguments can even come in any order.
Worked example
def greet(name, greeting="Hi"):
return greeting + ", " + name
print(greet(greeting="Yo", name="Ana"))Named arguments ignore position
Why: greeting and name are matched by name, so the reversed order still works.
Read the output
Why: Verified by execution: Yo, Ana.
| arg | goes to |
|---|---|
| greeting="Yo" | greeting |
| name="Ana" | name |
Trade off
Comparison matrix
From Keyword arguments: every row here is a choice with a cost. Fill the goes to column, then say which row you would actually pick and what you give up for it.
| arg | goes to |
|---|---|
| greeting="Yo" | greeting |
| name="Ana" | name |
Concept
A function that returns a bool reads like a question. Name it starting with is_ or has_: is_passing(score).
It slots straight into an if, which keeps your conditions short and readable.
Analogy
Discussion prompt
Explain Functions that answer yes/no by analogy to something with no Python Fundamentals in it at all — a queue, a recipe, a map, a bank balance, whatever fits. Then say where your analogy breaks.
Hint: An analogy that never breaks is not an analogy, it is the same idea wearing a hat. Find the seam — that is the part that is actually new.
Answer:
A function that returns a bool reads like a question. Name it starting with is_ or has_: is_passing(score).
Worked example
def is_passing(score):
return score >= 60
print(is_passing(72))
if is_passing(50):
print("pass")
else:
print("fail")It returns True or False
Why: is_passing(72) is True; is_passing(50) is False.
Reads naturally in an if
Why: Verified by execution: prints True, then fail.
| score | is_passing | if prints |
|---|---|---|
| 72 | True | - |
| 50 | False | fail |
Comparison
Comparison matrix
From An is_passing helper: refill the if prints column from what you know. The rest of the table is as it appeared.
| score | is_passing | if prints |
|---|---|---|
| 72 | True | - |
| 50 | False | fail |
Concept
A good function name is a short verb phrase: calculate_total, greet, is_even. The name should tell the reader what the call produces.
If you cannot name it in a few words, it may be doing too much - a hint we follow up on next session.
Counterexample
Discussion prompt
A good function name is a short verb phrase: calculate_total, greet, is_even. The name should tell the reader what the call produces.
That is stated as though it always holds. Do one of two things: produce a case where it fails, or say precisely what rules such a case out. "It just does" is not on the menu.
Hint: Hunt at the extremes first — zero, one, negative, empty, equal. If every extreme survives, the reason they survive is the proof.
Answer:
If you cannot name it in a few words, it may be doing too much - a hint we follow up on next session.
Section
Part 10
Pattern
1. def name(parameters): then an indented body
Why: Parameters are the inputs the body will use.
2. return the result (do not just print it)
Why: A returned value can be stored, compared, or reused; a printed one cannot.
3. Call it with name(arguments)
Why: The parentheses run it; arguments fill the parameters in order.
4. Define above the call, and reuse instead of repeating
Why: Python reads top to bottom. One function, many calls, one place to fix.
Pattern
Need the value later? return it
Why: Store it, compare it, feed it onward. This is the default for most functions.
Only showing a human? print is fine
Why: For a pure message with nothing to reuse, printing inside is okay.
Result comes out None? you forgot return
Why: The most common function bug - compute AND return.
Real world
Discussion prompt
Outside this lesson: where does Session 9 - Functions actually turn up? Name one concrete situation — a job, a piece of software someone ships, a decision somebody has to make — and say which part of return vs print, decided is doing the work in it.
Hint: Vague is the failure mode here. "Engineering" is not a situation; "deciding whether this build is fast enough to ship" is.
Answer:
Session 9 of the Python Fundamentals series, in depth. Packaging code into a named, reusable tool: def, parameters as inputs, return as output, calling with parentheses; the difference between return and print; functions with no return giving None; default arguments; functions calling functions; and refactoring a repeated block into one function.
Check
What comes back?
def add(a, b):
return a + b
print(add(10, 5))| a | b | returns |
|---|---|---|
| 10 | 5 | ? |
Check your understanding
What does this print?
Answer: A
Why: add(10, 5) returns 10 + 5 = 15, and print shows it. Verified by execution.
Check
Spot the missing return.
def triple(n):
n * 3
print(triple(4))| has return? | result |
|---|---|
| no | ? |
Check your understanding
What does this print?
Answer: A
Why: n * 3 is computed but never returned, so triple hands back None. Adding return n * 3 would print 12. Verified by execution.
Check
Can you reuse a printed value?
def show(n):
print(n)
x = show(5)
print(x)| show(5) prints | x holds |
|---|---|
| 5 | ? |
Check your understanding
What are the two lines of output?
Answer: A
Why: show prints 5 but has no return, so x is None; then print(x) shows None. To reuse the value, show would need to return n. Verified by execution.
Check
Which parameter gets which value?
def divide(a, b):
return a / b
print(divide(2, 8))| a | b | a / b |
|---|---|---|
| 2 | 8 | ? |
Check your understanding
What does this print?
Answer: A
Why: Arguments fill in order: a = 2, b = 8, so a / b is 2 / 8 = 0.25. Verified by execution.
Check
The second argument is omitted.
def power(base, exp=2):
return base ** exp
print(power(5))| base | exp | returns |
|---|---|---|
| 5 | ? | ? |
Check your understanding
What does this print?
Answer: A
Why: exp defaults to 2 when omitted, so power(5) is 5 ** 2 = 25. Verified by execution.
Connect it up
Draw it
One page, no notation unless you need it: draw how these connect — Naming a Block of Code · Parameters: Inputs · return: Output · return vs print · Functions with No return · Defaults & Combining. Put an arrow wherever one of them is what makes another possible, and label the arrow with why.
Recap
A function is a named block. def defines it, parameters are its inputs, return is its output, and parentheses call it.
| You write | It means |
|---|---|
| def add(a, b): | define a function with two inputs |
| return a + b | hand the result back |
| add(3, 4) | call it; the call becomes 7 |
| no return | the call is None |
| def f(x, y=2): | y defaults to 2 if omitted |
Return values you will reuse; print only for messages. Next session we go deeper - small single-job functions, local variables, and scope.
Want this taught 1-on-1? Alexander tutors Python Fundamentals — $55/session, free consultation.