Free Handbook · Runs in your browser

Functions

Defining and calling functions, arguments and defaults (and the mutable-default trap), *args and **kwargs, how scope actually works with a traced call stack, global and nonlocal, recursion, and lambda — with UnboundLocalError and RecursionError explained.

0 / 108 lessons🔥 0 day streak
ShareXLinkedIn

Module 04 · what you'll be able to do

  • Define functions with positional, keyword, default and variable arguments
  • Explain the LEGB scope rule and trace a call through the stack
  • Avoid the mutable default argument bug — the most famous Python gotcha
  • Write and reason about a recursive function, and know when it will blow the stack
  • Use lambda where it belongs: as a short key or callback
01

Defining and calling functions

A function packages a piece of work under a name so you can run it many times with different inputs. def creates it, return sends a value back. A function with no return (or a bare return) returns None — which is why print(list.append(x)) shows None.

pythonEdit it. ⌘/Ctrl + Enter runs.
You should see
12
20
Hi, Ada
None
49
Area of a rectangle.  ← a docstring: what the function is for.
Your turn
Write is_leap(year): divisible by 4, except centuries, except every 400th year. Test it with 1900, 2000, 2024.
Print versus return
A function that prints is a dead end — nothing else can use its result. A function that returns can be printed, stored, tested, and composed. Return values; print at the edges of your program.
02

Arguments and default values

pythonEdit it. ⌘/Ctrl + Enter runs.
You should see
db.local:5432 (timeout 30s)
db.local:6543 (timeout 30s)
db.local:5432 (timeout 5s)
100 EUR
Error you will hit

The mutable default argument (no error — a silent bug)

python
def add_item(item, basket=[]):
    basket.append(item)
    return basket

print(add_item("apple"))
print(add_item("pear"))    # expected ["pear"]
['apple']
['apple', 'pear']    ← the second call sees the first call's list
Why the interpreter said that

Default values are evaluated once, when def runs, not on each call. Every call without a basket gets the same list object, so it accumulates. This is the most-asked Python gotcha in interviews.

The fix

Use None as the default and create the list inside.

python
def add_item(item, basket=None):
    if basket is None:
        basket = []
    basket.append(item)
    return basket

print(add_item("apple"))
print(add_item("pear"))
Error you will hit

TypeError: area() missing 1 required positional argument: 'height'

python
def area(width, height):
    return width * height

print(area(3))
Traceback (most recent call last):
  File "your code", line 4, in <module>
    print(area(3))
TypeError: area() missing 1 required positional argument: 'height'
Why the interpreter said that

The function declares two parameters without defaults and was called with one. The error names exactly which parameter is missing. Its sibling, takes 2 positional arguments but 3 were given, is the opposite mistake — and inside a class it usually means you forgot self (Module 08).

The fix

Pass every required argument, or give the parameter a default.

python
def area(width, height=1):
    return width * height

print(area(3))
03

*args and **kwargs

A single star collects any extra positional arguments into a tuple; a double star collects extra keyword arguments into a dict. The names args and kwargs are convention — the stars are the syntax. The same stars in a call do the reverse: unpack a sequence or dict into arguments.

pythonEdit it. ⌘/Ctrl + Enter runs.
You should see
[INFO] user 42 logged in {'ip': '10.0.0.1', 'region': 'eu'}
[WARN]  {}
3
a | b!
[DEBUG] forwarded {'via': 'wrapper'}
Your turn
Write average(*numbers) that returns the mean of any number of arguments and returns 0 for none.
04

Scope: where a name lives

Every name is looked up in four places, in order — Local (inside the current function), Enclosing (any function this one is nested in), Global (the module), Built-in (print, len). Assignment inside a function creates a local name, always, even if a global with that name exists. That one rule explains every scope surprise.

VisualizeA call, step by step: locals are born and die with the callStep 1 / 7
rate = 0.2
def tax(amount):
total = amount * rate
return total
bill = tax(50)
print(bill)
Line 1

Module (global) scope: bind rate.

Variables now
rate0.2
All 7 steps as a table
StepLineWhat happenedVariables now
11Module (global) scope: bind rate.rate = 0.2
23def creates a function object and binds the global name tax. The body does NOT run yet.tax = <function>
37Call tax(50). A new local scope is created for this call; amount is bound to 50 inside it.amount (local) = 50
44Look up amount → found locally. Look up rate → not local, not enclosing, found in global scope: 0.2. Bind local total.total (local) = 10.0
55Return 10.0. The local scope is destroyed — amount and total no longer exist anywhere.amount (local) = (gone) total (local) = (gone)
67The returned value is bound to the global bill.bill = 10.0
78Print it.
Error you will hit

UnboundLocalError: cannot access local variable 'count' where it is not associated with a value

python
count = 0

def increment():
    count += 1      # reads count, then assigns count
    return count

increment()
Traceback (most recent call last):
  File "your code", line 7, in <module>
    increment()
  File "your code", line 4, in increment
    count += 1
UnboundLocalError: cannot access local variable 'count' where it is not associated with a value
Why the interpreter said that

Because count is assigned somewhere in the function, Python decides at compile time that count is local to the function. Then count += 1 tries to read that local before anything was assigned to it. Reading a global is fine; assigning to it makes it local.

The fix

Declare global count — or, far better, pass the value in and return the new one. Functions that reach out and modify globals are the hardest code to test.

python
def increment(count):
    return count + 1

count = 0
count = increment(count)
print(count)
pythonEdit it. ⌘/Ctrl + Enter runs.
You should see
changed by inner
global
2
LEGB
Local → Enclosing → Global → Built-in: the order names are searched. The first match wins.
global
Inside a function: "assignments to this name go to the module scope". Use sparingly.
nonlocal
Inside a nested function: "assignments to this name go to the nearest enclosing function's scope". The mechanism behind closures (Module 09).
05

Recursion

A recursive function calls itself on a smaller version of the problem, until it hits a base case it can answer directly. Every recursive function needs both: the base case that stops, and the step that shrinks. Miss the first and you get a RecursionError; miss the second and you get one too.

pythonEdit it. ⌘/Ctrl + Enter runs.
You should see
120
root
  a
    a1
  b
1000
Your turn
Write fib(n) recursively, then call fib(30) and notice how slow it is — that is the motivation for the memoisation you meet in Module 12.
Visualizefactorial(3) — the call stack grows, then unwindsStep 1 / 8
def factorial(n):
if n <= 1:
return 1
return n * factorial(n - 1)
print(factorial(3))
Line 6

Call factorial(3). Frame 1 opens with n = 3.

Variables now
stack[f(3)]
All 8 steps as a table
StepLineWhat happenedVariables now
16Call factorial(3). Frame 1 opens with n = 3.stack = [f(3)]
223 ≤ 1? No.
34Need factorial(2) first — frame 1 pauses at this line. Frame 2 opens with n = 2.stack = [f(3), f(2)]
442 ≤ 1? No. Need factorial(1). Frame 3 opens with n = 1.stack = [f(3), f(2), f(1)]
531 ≤ 1? Yes — base case. Return 1. Frame 3 closes.stack = [f(3), f(2)] f(1) = 1
64Frame 2 resumes: 2 * 1 = 2. Return 2. Frame 2 closes.stack = [f(3)] f(2) = 2
74Frame 1 resumes: 3 * 2 = 6. Return 6.stack = [] f(3) = 6
86Print the result.
Error you will hit

RecursionError: maximum recursion depth exceeded

python
def countdown(n):
    print(n)
    countdown(n - 1)     # no base case: never stops

countdown(3)
3
2
1
0
-1
... (about a thousand lines)
Traceback (most recent call last):
  File "your code", line 5, in <module>
  File "your code", line 3, in countdown
  File "your code", line 3, in countdown
  [Previous line repeated 996 more times]
RecursionError: maximum recursion depth exceeded
Why the interpreter said that

Each call adds a frame to the stack and nothing ever returns. Python caps the depth at 1000 to stop the process crashing. Even a correct recursive function will hit this on deep inputs (a 5,000-item linked list, say) — recursion in Python is for tree-shaped problems, not long sequences. Use a loop for those.

The fix

Add the base case.

python
def countdown(n):
    if n < 0:
        return
    print(n)
    countdown(n - 1)

countdown(3)
06

Lambda

A lambda is a one-expression anonymous function. Its whole reason to exist is being passed to something else — a sort key, a map, a callback — where naming a full def would be noise. If a lambda needs a second line, an if statement, or a name, make it a def.

pythonEdit it. ⌘/Ctrl + Enter runs.
You should see
36
['fig', 'kiwi', 'apple', 'banana']
['fig', 'kiwi', 'apple', 'banana']
banana
Cairo
[10, 20, 30]
[0, 2, 4, 6, 8]
Your turn
Sort [("Ada", 36), ("Bob", 36), ("Cy", 28)] by age descending, then by name ascending, in one sorted call.
Quick check

Which is the Pythonic choice for a reusable, named, three-line helper?

Finish the Python handbook, then get hired

Sit the exam for your certificate, run your resume through the ATS checker, and see the jobs that ask for exactly this.

Check my resume
Found this course useful? Share it.
ShareXLinkedIn

Comments

0

Join the conversation. Sign in to leave a comment — we'd love to hear your thoughts.