PYTHON / CHAPTER 10

Lexical scope and closures

Intermediate

Track which binding a nested function reads or changes.

By the end of this chapter

  • Distinguish rebinding from mutation
  • Use nonlocal deliberately
  • Avoid late-binding surprises in callbacks

The idea

A nested function can read names from its enclosing function. Assigning to a name normally creates a local binding in the assigning function, even when an outer name has the same spelling. nonlocal explicitly targets an existing enclosing-function binding. Mutating a captured list through its methods does not rebind the list’s variable and therefore follows a different rule.

Closures retain access to bindings rather than freezing every value when a function is created. Functions built in a loop can all read the loop variable’s final value when called later. A default argument can deliberately capture the current value for each function, because defaults are evaluated at definition time. Prefer a small factory when that intent would otherwise be obscure.

Python example
def counter():
    value = 0
    def step():
        nonlocal value
        value += 1
        return value
    return step
a, b = counter(), counter()
print(a(), a(), b())
Result
1 2 1

Each factory call creates a separate enclosing value; calls through a share one of them.

Try it yourself

Create a counter factory with a configurable starting value and independent state per factory call.

Use your own editor for this exercise. Runnable challenges on CodeQuest are available for JavaScript, Python, and SQL.

Open Python coding challenges

CHECK YOUR UNDERSTANDING

Chapter checkpoint

Which declaration permits a nested function to reassign its enclosing function’s count?