Lecture 4 of 1419 minInteractiveFree lecture

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.

InteractiveFrames on the call stackUse Step or Run below
Loading visual…

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:

InteractiveHow Python finds a nameClick a highlighted name
Loading visual…

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:

AnimationOne default list, shared by every call
Loading animation…
  1. Defined onceWhen def runs, Python creates the function object and evaluates the default [] right then. The list is stored on the function, in __defaults__.
  2. First calladd_item("apple") leaves basket out, so basket refers to the default list. append adds "apple" to it.
  3. Second calladd_item("bread") gets a new frame but the same default list, which still holds "apple". The result is ['apple', 'bread'].
  4. The fixUse None as the default. None is immutable, so sharing it between calls is harmless.
  5. 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 explained

Q1A function assigns count = 10 inside its body. A global variable is also called count. What happens to the global count?

Assignment inside a function creates a local name by default. The global variable is untouched — which is usually exactly what you want.

Q2What does a function return if it reaches the end without a return statement?

Every Python function returns something. Without an explicit return, it returns None — a common source of bugs when a function was supposed to return a value.

Q3Why is def add(item, basket=[]) dangerous?

Default values are evaluated once, when the function is defined. Every call that relies on the default mutates the same list, so items leak between calls. Use basket=None and create a new list inside.