Chapter 5 of 13 · Free, no signup

Support Us

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.

A function that calculates an answer and returns it without modifying outside variables is called a Pure Function. Pure functions are easy to test, debug, and reuse!

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

1: def calc_tax(price, rate=0.18):
2: tax = price * rate
3: return tax
4:
5: def checkout(cart_total):
6: tax = calc_tax(cart_total)
7: total = cart_total + tax
8: return total
9:
10: final_bill = checkout(100)
🥞 CALL STACK (TOP = RUNNING NOW)
<module>waiting
checkout(100)
STORE_STATUS = "OPEN"
checkout(cart_total=100)active
cart_total = 100
cart_total = 100

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)

Step 1: L · LocalYour Pocket

Variables created inside the current function. Destroyed when the function returns.

Step 2: E · EnclosingYour Room

Variables in an outer wrapper function (seen in nested functions and closures).

Step 3: G · GlobalThe House

Variables defined at the top file level. Accessible everywhere in this module.

Step 4: B · Built-inThe City

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 x
Golden Rule:

Python searches outward (Local ➔ Enclosing ➔ Global ➔ Built-in). It never searches into other functions' private locals!

To modify a global variable from inside a function, Python requires the explicit 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:

Function Signature:
def ship(item, city, cost=50):
Calling Code:
ship("Laptop", "Delhi", 40)

How Python Binds Each Argument

item➔ "Laptop"(1st positional argument)
city➔ "Delhi"(2nd positional argument)
cost➔ 40(3rd positional argument (overrode default 50))

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 cart

Test Calling the Function

Return Values Across Independent Calls:

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 = 1

Trap 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:

Coupon applied:
Step 1: calculate_subtotal()
Subtotal:$200.00
Step 2: apply_discount(subtotal, "SUMMER10")
Discount (10%):-$20.00
Step 3: calculate_tax(discounted, rate=0.18)
GST Tax (18%):+$32.40
Step 4: final_total()
Grand Total:$212.40

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.

Python
Line-by-line view
1tax_rate = 0.08
2def total_with_tax(price):
3 return price + price * tax_rate
4
5print(total_with_tax(25))

Names

Run the example to inspect local names.

Objects in memory

Memory snapshots appear as Python runs.

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=None for 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

Enjoying this free lesson? Your support helps us build more free animated chapters.

Support Us