Chapter 13 of 24 | Python

Scope, lifetime, and visibility

Scope describes where a name can be used. Python resolves a name using the LEGB rule: Local, Enclosing, Global, Built-in. Understanding this rule helps explain functions, nested functions, and closures.

message = "module level"

def show():
    message = "local"
    print(message)

show()          # local
print(message)  # module level

Assignment and the global/nonlocal statements

Assigning to a name anywhere in a function normally makes it local throughout that function, unless declared global or nonlocal. Reading the name before its local assignment raises UnboundLocalError. Avoid using global to share mutable application state; pass data explicitly or use an object that owns it.

nonlocal lets a nested function rebind a name in the nearest enclosing function scope. It is useful for closures, but a small class can be clearer when the state has several fields.

def make_counter():
    value = 0
    def increment():
        nonlocal value
        value += 1
        return value
    return increment

counter = make_counter()
print(counter(), counter())  # 1 2

Closures capture bindings

A closure retains access to names from its enclosing scope after the outer function returns. It captures the binding, not a frozen snapshot of the value. In loops, functions created in the loop may all observe the final value unless the value is bound at creation time.

functions = []
for number in range(3):
    functions.append(lambda number=number: number)
print([function() for function in functions])  # [0, 1, 2]

Comprehension and class scopes

Comprehension variables have their own scope in modern Python and do not leak into the containing scope. Class bodies execute in a namespace, but that namespace is not an enclosing lexical scope for methods. A method must access class attributes through the class or instance rather than expecting to capture a class-body local name.

Lifetime, cycles, and finalization

Names and objects have different lifetimes. Removing a local binding does not guarantee immediate destruction if another reference remains. CPython uses reference counting plus cyclic garbage collection, but Python does not promise a universal collection schedule. Use context managers for resources rather than relying on object finalization.

Beginner-to-master progression

Exercises

  1. Write a nested function that reads an enclosing value, then one that updates it with nonlocal.
  2. Demonstrate the loop-lambda late-binding issue and fix it with a default argument.
  3. Draw references between objects in a cyclic structure and explain why deleting one name may not free the objects immediately.

Global declarations and module boundaries

A module-level assignment creates or rebinds a name in that module's namespace. A function can read a global name, but assignment makes the name local unless declared global. This rule is determined by the function body, not by the branch taken at runtime.

count = 0

def increment():
    global count
    count += 1

increment()
print(count)

This works, but global mutable state couples callers and makes isolated tests harder. A class or explicit argument/return value often communicates ownership better. Use a global constant for configuration that truly does not change, and avoid using global as an easy substitute for program design.

Closures, late binding, and decorators

A nested function looks up an enclosing variable when it is called. The binding can change between creation and invocation. This “late binding” explains why callbacks created in a loop can all use the loop's final value. Binding a value through a default argument is one fix; a factory function that creates a new closure per iteration is another.

def make_multiplier(factor):
    def multiply(value):
        return value * factor
    return multiply

double = make_multiplier(2)
triple = make_multiplier(3)

Closures are a foundation of decorators and functional composition. They can also retain large objects longer than expected. If a callback or wrapper is stored in a long-lived registry, inspect which outer values it captures and whether that retention is intentional.

Class namespaces and descriptors

A class body executes to create a namespace of attributes, which the metaclass turns into a class object. A method function stored in the class becomes a bound method when accessed through an instance; the descriptor protocol supplies the instance automatically. Class variables are shared attributes unless shadowed on an instance. A mutable class variable is therefore shared state, not a per-instance default.

class Example:
    labels = []  # shared by all instances: usually a bug

    def __init__(self):
        self.own_labels = []  # separate list for each instance

Mastery checkpoint

  1. Explain the LEGB resolution of each name in a nested function.
  2. Find and fix an unintended closure capture that retains a large object.
  3. Demonstrate the difference between a class attribute and an instance attribute.
  4. Use dis to inspect bytecode as an implementation experiment, then explain why bytecode is not a stable language contract.
0% read