Environment Diagrams

How Python binds names, builds frames, and creates closures

Why we need an environment model

  • Last time: function values, passed as arguments, returned as results
  • Today: where names live, and how Python finds them
  • Destination: read closures via frames $+$ parent pointers

Start simple: names in one frame

x = 2
y = x + 1
print(y)
  • Global frame: bindings like x -> 2, y -> 3
  • Evaluation: compute value first, then bind name

Frames: the tables Python uses for names

x = 2
# inside a function: x = 99
  • Frame $=$ table of bindings: name $->$ value
  • Environment $=$ current frame $+$ parent chain
  • Name lookup: search current, then parent, then keep going

Calling a function creates a new frame

def square(x):
    return x * x

print(square(3))
print(square(square(3)))
  • Call frame: parameter binding x -> 3 (then another call with x -> 9)
  • Parent of call frame: where square was defined (global here)

Lookup rule explains shadowing

x = 1

def f(x):
    return x + 1

def g():
    return x + 1

print(f(10))
print(g())
  • Shadowing: local x hides global x inside f
  • Free name: g finds x by walking to global

Closures: a function plus its parent environment

def make_adder(n):
    def adder(x):
        return x + n
    return adder

add_three = make_adder(3)
print(add_three(4))
  • Closure: adder carries a pointer to the frame where n -> 3
  • Call to add_three(4): local x -> 4, then lookup finds n in parent

lambda creates the same kind of function object

f = lambda x: x + 1
print(f(5))
print((lambda x: x * x)(5))
  • lambda vs def: same closure idea, different syntax
  • Parent environment: wherever the lambda was evaluated

Two closures can share one piece of local state

def make_counter():
    n = 0
    def inc():
        nonlocal n
        n += 1
        return n
    def get():
        return n
    return inc, get

inc, get = make_counter()
print(inc())
print(inc())
print(get())
  • Shared parent frame: both inc and get close over the same n
  • Preview: nonlocal updates an existing binding (mutation topic later)

What environment diagrams buy you

  • Debugging: ask "which frame binds this name?"
  • Closures: function value includes its parent environment
  • Next: recursion $=$ many frames over time

Exit ticket: closure vs global name

def make_adder(n):
    def adder(x):
        return x + n
    return adder

add_three = make_adder(3)

# Predict before running:
# What will this print?

n = 100
print(add_three(10))
  • Practice: predict the printed value
  • Reflection: one sentence on why changing global n does not matter here

Thank you for watching!

Closing illustration

Like, Share, and Subscribe to Thinking in Math

Powered by SciMigo AI Tutor - https://scimigo.com