Author: Amin Boulouma, Software Engineer Github source code: https://github.com/aminblm/ai_systems_design_from_scratch Engineering Blog: https://aminblm.github.io/ai_systems_design_from_scratch/blog/
7 Reasons You Should Stop Misunderstanding Closures & Scope (Without Complex Theory)
In Python, scoping is often treated as a “magic” behavior. Why can a function access variables from its outer scope? How do closures preserve data even after a function has returned? These are the foundational concepts behind decorators and function factories. Mastering these will stop your code from leaking state and prevent common bugs in your functional pipelines.
The Core Concept
Scope refers to the region of your code where a variable is accessible (LEGB rule: Local, Enclosing, Global, Built-in). A Closure is a nested function that “remembers” the variables from its enclosing scope, even after the outer function has finished executing.
Glossary for Beginners
- LEGB: The order in which Python searches for variables: Local, Enclosing, Global, Built-in.
- Closure: A function object that stores its lexical environment (captured variables).
- Nonlocal: A keyword used to modify a variable defined in the enclosing (non-global) scope.
- Lexical Scope: The determination of variable accessibility based on the physical structure of the code.
Why We Choose Closures Over Class-Based State
We choose closures when we need to maintain state for a single function without the overhead of defining a full class. It provides clean, private data encapsulation for small, reusable units of logic.
Why X over Y? We choose closures over global variables to prevent “spooky action at a distance.” Closures keep state local to the function, making it immutable and thread-safe. We use nonlocal if the internal function must update the captured variable, allowing for sophisticated stateful generators.
Implementation: The Closure Pattern
Simple Example: The Function Factory
def multiplier_factory(factor: int):
# 'factor' is captured in the closure
def multiplier(n: int):
return n * factor
return multiplier
double = multiplier_factory(2)
print(double(10)) # Output: 20
Complex Example: Production-Grade Stateful Decorator
In production, we use closures to create wrappers that track call counts or handle rate limiting without using global state.
from typing import Callable
def count_calls(func: Callable):
count = 0 # Captured variable in closure
def wrapper(*args, **kwargs):
nonlocal count
count += 1
print(f"Call {count} for {func.__name__}")
return func(*args, **kwargs)
return wrapper
@count_calls
def process():
pass
process()
process() # Output: Call 1 for process, Call 2 for process
Quick Reference: Scoping Rules
| Scope | Keyword | Behavior |
|---|---|---|
| Local | None | Inside the current function |
| Enclosing | nonlocal |
Parent function’s scope |
| Global | global |
Module-level variables |
| Built-in | None | Pre-defined Python functions |
Developer Checklist
- Are you unintentionally using global variables when a closure could isolate the state?
- Did you use
nonlocalif you need to mutate a variable in the enclosing scope? - Are your closures capturing expensive objects? (Be mindful of memory leaks).
- Is your logic complex enough that a
classwould actually be more readable?
TL;DR Summary
Stop using global variables to track state. Closures offer a powerful, encapsulated way to preserve data across function calls, keeping your code clean and decoupled. Master the LEGB rules, and you will understand how to build everything from decorators to sophisticated middleware. Use nonlocal judiciously, and always prefer functional purity over shared mutable state.