Functions and scope
Functions turn a block of code into a named, reusable, testable tool. Watch the call stack grow and shrink as functions call each other, learn how Python finds names, and avoid the most famous function-related bug in Python.
In this lecture you will
- Define functions with parameters, default values and return values
- Explain the call stack and what happens on each call and return
- Apply the LEGB rule to predict which variable a name refers to
- Avoid the mutable default argument pitfall
As scripts grow, the same logic appears in several places, and long stretches of code become hard to follow. Functions solve both problems: they give a block of code a name, a clear set of inputs and an output. Well-named functions make code read like a description of what it does.
Defining and calling functions
def gross_price(net, tax_rate=0.18):
"""Return the price including tax."""
return round(net * (1 + tax_rate), 2)
print(gross_price(100)) # 118.0 — uses the default rate
print(gross_price(100, 0.05)) # 105.0
print(gross_price(net=250, tax_rate=0.12)) # keyword arguments
- Parameters (
net,tax_rate) are the names a function uses for its inputs; arguments are the values passed in a call. - Default values make parameters optional.
- Keyword arguments name each value, making calls self-explanatory and order-independent.
- return sends a value back to the caller and ends the function. Without a return statement, the function returns
None. - The string on the first line is a docstring; editors and
help()display it.
The call stack
Each time a function is called, Python creates a frame: a fresh space for that call’s parameters and local variables. Frames stack up as functions call other functions, and each one disappears when its function returns. Step through the example to watch it happen.
Two things are worth noticing. First, area and total_area both have variables called w and h, yet they never interfere — each frame has its own. Second, all of a function’s local variables vanish when it returns; only the returned value survives.
This is also what a traceback shows when something fails: the frames that were on the stack at that moment, outermost first.
Scope: how Python finds a name
When you use a name, Python searches four scopes in order — the LEGB rule:
- Local
- Names assigned inside the current function, including its parameters.
- Enclosing
- Names in any function that contains this one (for nested functions).
- Global
- Names assigned at the top level of the module (the file).
- Built-in
- Names Python provides everywhere, such as len, print and range.
Click the names below to watch Python search the scopes in order:
Python decides which names are local when it compiles the function, not while it runs. That is why the second example fails even though a global count exists.
An important consequence: assigning to a name inside a function makes it local to that function. Reading a global variable inside a function works, but assigning to it creates a new local variable instead. The global keyword can override this, but code is almost always clearer when functions receive what they need as parameters and return their results.
The mutable default trap
Default values are evaluated once, when the function is defined — not on each call. With immutable defaults such as numbers and strings this doesn’t matter. With a list or dict, it does:
def add_item(item, basket=[]): # the same list for every call!
basket.append(item)
return basket
add_item("apple") # ['apple']
add_item("bread") # ['apple', 'bread'] — surprise
Where does the extra "apple" come from? Watch the objects:
- Defined onceWhen def runs, Python creates the function object and evaluates the default [] right then. The list is stored on the function, in __defaults__.
- First calladd_item("apple") leaves basket out, so basket refers to the default list. append adds "apple" to it.
- Second calladd_item("bread") gets a new frame but the same default list, which still holds "apple". The result is ['apple', 'bread'].
- The fixUse None as the default. None is immutable, so sharing it between calls is harmless.
- A new list per callbasket = [] now runs on every call, so each call builds and returns its own list.
The standard fix is to use None as the default and create a new object inside:
def add_item(item, basket=None):
if basket is None:
basket = []
basket.append(item)
return basket
Designing good functions
- Do one thing, and name the function after it:
parse_invoice,send_alert. - Prefer inputs and outputs over side effects. A function that takes data and returns a result is easy to test and reuse; one that quietly modifies globals is not.
- Keep them short — if you need to scroll to read a function, it probably does several things.
In the final lecture of this module, we look at Python’s built-in collections — and at the clever data structure that makes dictionaries so fast.
Key takeaways
- A function call creates a new frame for its local variables; returning removes it.
- Parameters and variables assigned inside a function are local — they don’t clash with names elsewhere.
- Python looks names up in Local, Enclosing, Global and Built-in scopes, in that order.
- Never use a mutable object such as a list as a default argument; use None and create it inside.
Check your understanding
3 questions · answers explainedQ1A function assigns count = 10 inside its body. A global variable is also called count. What happens to the global count?
Q2What does a function return if it reaches the end without a return statement?
Q3Why is def add(item, basket=[]) dangerous?