Where a name lives
The scope of a name is the part of the program where it can be used. A name created inside a function is local: it exists only while that function runs and is invisible outside. A name created at the top level of the file is global: functions can read it. When Python meets a name, it looks first in the function, then in any enclosing function, then in the file, then in the built-ins.
rate = 0.001 # global: defined outside any function
def fee(notional):
minimum = 1.0 # local: exists only during the call
return max(minimum, notional * rate)
print(fee(36850.0)) # 36.85
# print(minimum) # would raise NameError: minimum is not definedAssigning makes a local name
Reading a global from inside a function is fine. But assigning to a name inside a function creates a new local name, even if a global with the same name exists: the global is left alone. That protects your data, and it is also why changing global state from a function takes extra words (global) that you should rarely need. Pass values in as arguments and give results back with return.
# Assigning inside a function makes a new local name
balance = 1000
def deposit(amount):
balance = amount # a different, local balance
return balance
print(deposit(50)) # 50
print(balance) # 1000 (the global one is unchanged)Functions that make functions
A function can define another function inside itself and return it. The inner function keeps access to the variables of the outer one, even after the outer function has finished. That bundle of a function and the values it remembers is a closure. It is a neat way to build a ready-made tool from settings: one calculator for stocks, another for ETFs.
# A closure remembers the variables of the function that made it
def make_fee_calculator(rate, minimum):
def fee(notional):
return max(minimum, notional * rate)
return fee
stock_fee = make_fee_calculator(0.001, 1.0)
etf_fee = make_fee_calculator(0.0005, 0.5)
print(stock_fee(36850.0)) # 36.85
print(etf_fee(36850.0)) # 18.425State that lives between calls: nonlocal
A closure can also change what it remembers. Since assigning would create a new local name, the inner function declares nonlocal count first: "this name belongs to the enclosing function". Each call of the outer function creates its own separate memory, so two counters never interfere. This is how an order-id generator or a running average keeps its state without any global variable.
# nonlocal lets the inner function rebind the outer variable
def make_counter():
count = 0
def next_id():
nonlocal count
count += 1
return count
return next_id
order_ids = make_counter()
print(order_ids(), order_ids(), order_ids()) # 1 2 3
other = make_counter()
print(other()) # 1 (its own count)# State between calls: an exponential moving average
def make_ema(alpha):
value = None
def update(price):
nonlocal value
value = price if value is None else alpha * price + (1 - alpha) * value
return value
return update
ema = make_ema(0.5)
print(ema(100.0), ema(110.0), ema(110.0)) # 100.0 105.0 107.5- Inside a function, names you assign are local; pass data in,
returndata out. - A function that builds and returns a function packages settings into a tool.
- Use
nonlocalfor state that must survive between calls, one memory per closure.
Next up: list comprehensions, to build a new list from another in a single, readable line.
Lab · what's graded
- Build a counter that remembers its count with
make_counter25 pts - Package fee settings into a function with
make_fee_calculator25 pts - Keep a moving average between calls with
make_ema25 pts - Create a price check from a limit with
make_limit_checker25 pts