Python Closures & Scope
Master the concept step by step with clear explanations, examples, and code you can run.
Advanced Python Closures & Scope: Mastering Python's Memory Brain
Hi there! Welcome back.
Grab a comfortable seat. At the end of our last chat about dunder methods, we completely opened up Python's engine. We ended that lesson by asking a very deep question: How does Python remember data when functions are wrapped inside other functions? If you define a variable inside a method, why can't you access it outside?
Today we're pretty much going to answer that by looking directly into Python's "brain." We're pretty much going towards explore Scope, The LEGB Rule, and the magical world of Closures.
If you have probably ever felt confused by an error saying variable is "referenced before assignment," or wondered how advanced developers write super clean, memory-efficient code without always using massive classes you're pretty much in the right place. Let's dive in!
1, while the Concept with Scope (The One-Way Mirror)
Imagine a large house with a lot of rooms, while if you're pretty much standing in kitchen, you can see the apples on the counter. But if you walk out into a street and close front door, you can no longer see those apples, while
in Python, Scope works exactly like this. It's the "area" of your code where a specific variable is visible and alive.
When you create variable inside a function it is actually trapped in that function's room. Professional developers call this Local Scope. Once the function finishes running, Python immediately deletes that variable to save memory, and
but what happens when you have actually functions inside of functions? Or variables floating outside your functions?
2. The LEGB Rule: How Python Searches for Names
When you type a variable name, Python has to figure out what data you are actually talking about. To do simply this, it follows a strict sequence called the LEGB rule.
As highlighted in a recent July 2025 exploration of resolving names in Python, Python fix variable names by searching through distinct layers in a very specific order.
Here is the exact sequence Python follows beautifully outlined in Codefinity's guide on understanding scopes:
- L - Local: Python first looks inside the current function you're actually working in.
- E - Enclosing: If it can't find it there it looks at any "parent" function that wraps around your current function.
- G - Global: Still nothing? It checks top level of your script (variables defined outside all functions).
- B - Built-in: Finally, it checks Python's pre-installed names (like
print(),len(), orException).
Here is a visual map of how Python's brain searches towards the variable:
flowchart TD
A[1. Local Scope] -->|If not found| B[2. Enclosing Scope]
B -->|If not found| C[3. Global Scope]
C -->|If not found| D[4. Built-in Scope]
D -->|If not found| E(((NameError!)))
style A fill:#4caf50,stroke:#388e3c,color:#fff
style B fill:#2196f3,stroke:#1976d2,color:#fff
style C fill:#ff9800,stroke:#f57c00,color:#fff
style D fill:#9c27b0,stroke:#7b1fa2,color:#fff
style E fill:#f44336,stroke:#d32f2f,color:#fff
If Python reaches the bottom and still can't find your variable it throws a loud NameError and crashes!
3. Closures: Functions using Backpacks
Now things get really interesting.
What happens if a parent function finishes running but it returns the child function? Remember, usually when a function finishes, Python destroys all of its local variables, while
let's look at some code:
def financial_account(initial_balance):
# This is the ENCLOSING scope
balance = initial_balance
def withdraw(amount):
# This is the LOCAL scope
if amount > 0 and amount <= balance:
return f"Success! Withdrew ${amount}"
return "Failed."
return withdraw # We are returning the child function itself!
# Let's create an account
my_transaction = financial_account(100)
# Now let's use the child function
print(my_transaction(40))
Wait second.
financial_account(100) finished running. That means a balance variable should probably be dead and destroyed right, while
but when we call my_transaction(40) it somehow still knows that the balance is 100! How?
This is called Closure.
Think with a closure as a function carrying a little invisible backpack. When the child function (withdraw) is born inside a parent function, it looks around and says, "I might need that balance variable later." So it packs balance into its backpack, while even after the parent function dies, child function still carries that saved data around.
4. The nonlocal Superpower
Closures are amazing for hiding data safely. Because balance is hidden in a backpack no other developer can accidentally change it out of the outside. It's perfectly secure!
But what if our withdraw function needs to update the balance inside its backpack;
if we try to write balance = balance - amount inside our local function Python will panic; by default Python thinks you are really trying towards create the brand-new Local variable but you haven't assigned it value yet!
To fix this, we have to tell Python to reach up into the Enclosing scope. According to an insightful 2024 deep-dive on mastering Python closures, the nonlocal keyword is exactly what you need to modify a variable in that middle enclosing layer.
Let's fix our bank account:
def financial_account(initial_balance):
balance = initial_balance
def withdraw(amount):
nonlocal balance # Reaching into the backpack!
if amount > 0 and amount <= balance:
balance -= amount
return f"Success! New balance is ${balance}"
return "Insufficient funds."
return withdraw
my_account = financial_account(100)
print(my_account(40)) # Prints: Success! New balance is $60
print(my_account(40)) # Prints: Success! New balance is $20
By typing nonlocal balance we told Python's brain: "Don't really create the new local variable, while go up to the Enclosing scope and change the data we saved on our closure backpack."
5. Trade-offs: When to use Closures vs, and oop Classes
As an intermediate developer you need to know the trade-offs of a tools you use.
Why use a Closure when you could just write a whole BankAccount Object-Oriented class?
- Lightweight & Fast: If you only have just one single method (like
withdraw), building a massive class with__init__andselfis overkill; a closure uses less code and is slightly faster. - Data Privacy: Closures hide variables completely, and there is absolutely no way to access
balancedirectly out of a global scope.
The Danger: Memory Leaks You've got to be careful with closures; because the child function "remembers" the enclosing environment, it keeps those objects alive in your computer's memory.
If you trap massive amounts of data inside a closure (like huge server logs you opened using object-oriented filesystem paths), a garbage collector can't clean it up; over time, your application will leak memory. Always be conscious of what you're actually putting in that invisible backpack!
What's Next;
you did a phenomenal job today. You now completely get how Python's brain sort out variable names using the LEGB rule. You also learned how to use closures and the nonlocal keyword to build incredibly secure lightweight functions that remember data without needing massive OOP architectures;
but what if we could use this closure superpower to change how other functions behave; what if we could probably write a closure that wraps around a function, automatically measuring how fast it runs, or automatically catching exceptions, without ever changing the original code?
That is exactly what we'll cover next. Into our next chapter we're pretty much going for dive into Python Decorators. Get ready to learn one of the most powerful and heavily used features in modern Python! See you there.