Skip to the content.

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


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

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.