Python resolves a name through its enclosing namespaces, while assignment inside a function normally binds a local name.
Python scope: local assignment, nonlocal state and late binding
Operation contract
The progress counter keeps one integer inside its closure and uses nonlocal to rebind that enclosing name. The label callbacks capture their current receipt ID through a default argument. These mechanisms solve different problems: nonlocal gives several calls shared state; the bound default gives each callback its own selected value. A closure over a loop variable without that binding would read its final value when called later.
Failure and ownership boundary
The counter is not thread-safe. Its read, addition and replacement form one application transition but not one protected transaction. An immutable captured integer is safe from later mutation; capturing a mutable record would still retain its alias. Python closures: capture loop values at the intended time and Python locks: protect the complete inventory transition make those boundaries concrete.
Working program
def progress_counter():
completed = 0
def record_completion():
nonlocal completed
completed += 1
return completed
return record_completion
record = progress_counter()
labels = [lambda receipt_id=receipt_id: receipt_id for receipt_id in (41, 42)]
print(record(), record())
print([label() for label in labels])Output
1 2
[41, 42]Costs and limits
Each closure retains its reachable captured objects. Creating n label callbacks takes O(n) time and O(n) retained function/default storage. Growing integer counts also incur digit costs.
Common Mistakes
- nonlocal changes an enclosing binding, not a global variable.
- A captured reference does not freeze the referenced object.
Connected lessons
Python closures: capture loop values at the intended time, Python function arguments: avoid shared mutable defaults, Python locks: protect the complete inventory transition.
