Closures and deletion in Python highlight one of the most elegant aspects of the language’s scoping and memory model — a closure (a nested function that remembers its enclosing scope) keeps variables from that scope alive as long as the closure itself exists in memory, even after the outer function has returned and its local scope would normally be deleted. Deleting the outer function name (or letting it go out of scope) does not destroy the closure’s captured variables — they persist because the closure holds a reference to them. In 2026, understanding closure lifetime and deletion behavior is crucial — it explains memory retention in long-lived callbacks, stateful decorators, generator factories, pandas/Polars custom transformers, and any code with persistent nested functions, while also helping avoid accidental memory leaks when closures outlive their intended scope.
Here’s a complete, practical guide to closures and deletion in Python: how closures capture and retain state, lifetime after outer scope deletion, garbage collection rules, real-world patterns, and modern best practices with type hints, weak references, resource management, and pandas/Polars integration.
Basic closure lifetime — inner function captures outer variables; they live as long as any reference (including the closure) exists.
def outer_function(x: int):
captured = x * 2
def inner(y: int) -> int:
return captured + y # inner closes over 'captured'
return inner
closure = outer_function(5)
print(closure(3)) # 13 (captured = 10 + 3)
del outer_function # outer name deleted — closure still works
print(closure(3)) # still 13 — captured variable lives on
Deletion and garbage collection — variables are kept alive only by references; when no references remain (including from closures), they are collected.
def make_closure():
data = [1, 2, 3] # list object created
def inner():
return sum(data)
return inner
cl = make_closure()
print(cl()) # 6
del cl # last reference to closure gone
# data list is now eligible for garbage collection
Real-world pattern: long-lived closures in pandas/Polars pipelines — captured state persists for repeated transformations or callbacks.
def make_normalizer(mean: float, std: float):
def normalizer(col):
return (col - mean) / std # captures mean/std
return normalizer
# Create once, reuse many times
norm_sales = make_normalizer(df['sales'].mean(), df['sales'].std())
df['sales_norm'] = norm_sales(df['sales'])
# Even after df is modified or goes out of scope, norm_sales remembers original stats
Best practices make closures safe, readable, and memory-efficient. Use closures intentionally — for stateful callbacks, factories, decorators — but avoid accidental long-lived captures that leak memory. Modern tip: use Polars for immutable-by-default DataFrames — transformations return new frames, reducing need for mutable captured state. Add type hints — def make_adder(n: int) -> Callable[[int], int] — improves clarity and mypy checks. Prefer explicit parameters over captures when possible — clearer and avoids lifetime surprises. Use weakref for non-essential references — prevent memory leaks when closures should not keep objects alive. Avoid capturing large objects (DataFrames, models) — pass them explicitly or use weak references. Test closure lifetime — assert captured values correct after outer scope deletion. Use del or gc.collect() in tests — verify cleanup. Combine with @functools.lru_cache on returned functions — memoize expensive closures. Refactor long-lived closures to classes — better encapsulation, explicit lifecycle. Use contextvars in async code — per-context state without globals/closures.
Closures keep enclosing scope variables alive as long as the closure exists — even after outer function deletion — thanks to reference counting. In 2026, use closures for stateful factories/callbacks, avoid accidental leaks, prefer explicit parameters, type hints, weak references, and Polars immutability. Master closure lifetime and deletion, and you’ll write memory-safe, predictable code that handles long-lived functions without surprises.
Next time a nested function needs outer state — create a closure. It’s Python’s cleanest way to say: “This function remembers its birthplace — even after home is gone.”