Scope and flow of execution

Part of the free LogicWiz Python course, Module 3: Functions. The examples and practice run in the app.

Meher kept the day's sales in sales = 0 and wrote add_sale() to do sales = sales + 50. It crashed with UnboundLocalError. The function could read sales a minute ago. Why can't it change it?

You'll learn to

  • Tell local names from global names
  • Explain UnboundLocalError and fix it
  • Use `global` when you must, and prefer returning a value
  • Follow the flow of execution through function calls

Learn

Each call gets its own notebook

When a function runs, Python opens a fresh local scope for it, like a rough notebook for that one call. Parameters and any name assigned inside the function live there. When the function returns, the notebook is thrown away.

Names made at the top level of the file are global: every function can read them.

Reading a global is fine

rate = 5

def with_gst(amount):
    tax = amount * rate / 100
    return amount + tax

print(with_gst(200))
print(rate)

Step through: amount and tax appear while with_gst runs and vanish after it returns. rate is global and stays.

Locals don't leak out

def with_gst(amount):
    tax = amount * 5 / 100
    return amount + tax

print(with_gst(200))
print(tax)

NameError: name 'tax' is not defined. tax belonged to that one call and is gone.

Assigning inside makes a new local

x = 10

def change():
    x = 99
    print("inside:", x)

change()
print("outside:", x)

x = 99 inside the function creates a local x. The global x is untouched: same name, two different boxes.

Meher's crash

sales = 0

def add_sale(amount):
    sales = sales + amount
    return sales

add_sale(50)

Why UnboundLocalError? Python decides which names are local before the function runs: any name assigned anywhere in the function is local for the whole function. So in sales = sales + amount, the right side reads the local sales, which has no value yet. sales += amount fails the same way.

Two fixes: global, or return

sales = 0

def add_sale(amount):
    global sales
    sales = sales + amount

add_sale(50)
add_sale(30)
print(sales)

def add_sale2(total, amount):
    return total + amount

total = 0
total = add_sale2(total, 50)
total = add_sale2(total, 30)
print(total)

global sales says "use the file's sales". It works, but the second style (pass in, return out) is easier to test and reuse. Use global sparingly.

Flow of execution: calls stack up

def tax(x):
    print("  tax starts")
    return x * 0.05

def bill(x):
    print(" bill starts")
    t = tax(x)
    print(" bill ends")
    return x + t

print("main starts")
print(bill(100))
print("main ends")

Python reads the two defs first (just remembering them), then runs from print("main starts"). A call pauses the caller; return resumes it exactly where it left off. That pile of paused calls is the call stack.

CBSE asks flow of execution as a list of line numbers. The rule: the def header line is visited once when defined; on each call, jump to the header, run the body, then come back to the calling line. Number the lines in the margin and follow your pencil.

In one line

Names assigned inside a function are local to that call; reading a global is fine, assigning to it needs global (or better, return the new value).