Python Mastery · Chapter 5 · ~22 min · Intermediate
Functions, Scope & the Call Stack
Functions are your program's building blocks. Step inside Python's memory engine to see how stack frames push and pop, how the LEGB rule tracks down variables, and how to harness *args, **kwargs, and clean defaults.
- Visualize stack frames push and pop in real time
- Master LEGB variable scope search order
- Handle positional, keyword, *args and **kwargs
- Avoid mutable default arguments and UnboundLocalError
1Functions as clean machines
Think of a function as a sealed machine in your factory. Raw materials go in (parameters), it does internal work in private, and it drops a finished product out of the chute (return).
Inputs (Parameters)
What the function needs to do its job. Can have default fallback values.
Private Workroom (Body)
Local variables live here temporarily. Outside code cannot see or touch them.
Output (Return)
The answer sent back to whoever called it. If omitted, Python returns None.
2Under the hood: The Call Stack
When you call a function, Python places a new tray (called a stack frame) on top of the Call Stack. When the function returns, its tray is popped off and recycled. Step through the simulation below:
Call Stack Engine (LIFO: Last In, First Out)
Step 1 of 4
Source Code
Program starts at module level. checkout(100) is called. Pushing checkout() frame onto the stack!
What happens to local variables when a function returns?
3Variable Scope & the LEGB Rule
When Python encounters a variable name like x, where does it look? It checks four nested rooms in strict order: Local ➔ Enclosing ➔ Global ➔ Built-in.
The LEGB Lookup Ladder (Inside ➔ Outside)
Variables created inside the current function. Destroyed when the function returns.
Variables in an outer wrapper function (seen in nested functions and closures).
Variables defined at the top file level. Accessible everywhere in this module.
Pre-installed Python tools available anywhere automatically: len, sum, print, range.
How Python finds the variable
# Searching for "x" inside checkout():
# 1. Found right here in Local scope! Won't look any further.
def checkout():
x = 100 # 📍 Local to checkout
return xPython searches outward (Local ➔ Enclosing ➔ Global ➔ Built-in). It never searches into other functions' private locals!
global x keyword. But beware: modifying globals is a major source of bugs — prefer returning values instead!What does this print?
x = 5
def test():
x = 10
test()
print(x)4Arguments superpower: Positional, Keyword, *args & **kwargs
Python gives you unmatched flexibility when passing values into functions. Switch modes below to see how arguments bind:
How Python Binds Each Argument
Positional arguments bind strictly by order: first goes to first, second to second.
What type is *args inside the function body?
5The 4 classic function traps
Trap 1 — The Mutable Default Argument Bug. Never write def fn(cart=[]):. Default arguments are evaluated only ONCE when Python loads the file. Everyone shares the same list!
# ❌ THE TRAP: cart=[] evaluates ONCE when def is loaded!
def add_to_cart(item, cart=[]):
cart.append(item)
return cartTest Calling the Function
Press a button above to run calls.
Trap 2 — Forgetting return. If you write print(result) instead of return result, your function hands back None to the caller.
Trap 3 — UnboundLocalError. If you try to read a global variable and then assign to it lower in the same function, Python marks it local for the entire function:
# ❌ UnboundLocalError: local variable 'count' referenced before assignment
count = 0
def bump():
print(count) # Python thinks count is local because of the line below!
count = 1Trap 4 — Infinite recursion (Stack Overflow). A function that calls itself without an exit condition pushes frames until Python halts with RecursionError: maximum recursion depth exceeded.
What is the recommended fix for def add(item, cart=[]):?
6Real data: Modular pricing pipeline
In production apps, long spaghetti scripts are broken down into small, single-responsibility functions. See how this modular checkout pipeline computes discounts and taxes cleanly:
Pure, Testable Functions
def apply_discount(amount, code="NONE"):
rates = {"SUMMER10": 0.10, "VIP20": 0.20}
return amount * (1 - rates.get(code, 0.0))
def calculate_tax(amount, rate=0.18):
return amount * rate
def build_order_receipt(subtotal, code="NONE"):
discounted = apply_discount(subtotal, code)
tax = calculate_tax(discounted)
return {
"subtotal": subtotal,
"tax": round(tax, 2),
"total": round(discounted + tax, 2),
}Test clean helper functions and data converters right in your browser with the Python Playground.
7Challenge: Function & Scope Architect
Answer all 5 architecture scenarios correctly to complete Chapter 5 and unlock your mastery badge!
Functions, Scope & the Call Stack · code lab
Edit the example, run it with Python, or step through the lines.
1tax_rate = 0.082def total_with_tax(price):3 return price + price * tax_rate45print(total_with_tax(25))
Names
Objects in memory
Python output
Run the code to see its output.
1. A function needs to accept any number of extra tags or keywords without crashing
2. You want a default list parameter that doesn't share items between separate function calls
3. A function has no 'return' statement at the bottom. What does it return when called?
4. A variable is defined inside a function. Can another function access it directly?
5. What data structure does Python use to keep track of active function calls and where to return?
🎯 0 / 5 correct
Try the full parameter-packing and pure function script in the Python Playground (code prefilled).
8Recap
- Call Stack: Functions push frames when called and pop frames when returning (LIFO).
- LEGB Rule: Variable lookup travels Local ➔ Enclosing ➔ Global ➔ Built-in.
- Default parameters: Always use
param=Nonefor lists or dicts to avoid shared state. - *args packs extra positional items into a tuple; **kwargs packs extra named items into a dict.
- Aim for Pure Functions: take inputs, return outputs, avoid mutating outside state.
Finish the challenge above to complete the chapter, or