Most Python tutorials explain what scopes, closures, and decorators are. But let’s be honest… they often feel like boring manuals.
Today, instead of a dry lecture, we’re going to sneak backstage 🎭, peek into Python’s engine room 🛠️, and uncover the magic behind these concepts in a way that actually sticks in your brain.
By the end, you’ll see why Python feels like a language that bends reality and you’ll never look at a decorator the same way again.
Grab your popcorn 🍿 and let’s dive in.
🎯 Part 1: Scopes, The Treasure Map of Variables
Think of Python as a giant mansion 🏰. Each room (function/module) has drawers filled with valuables (variables).
When you ask for x
, Python searches the drawers in this order:
- Local (L) → Your room (the current function).
- Enclosing (E) → Any outer function enclosing the current one.
- Global (G) → The entire mansion (module-level namespace).
-
Built-in (B) → The outside world (Python’s built-in functions like
len
,print
).
This is called the LEGB Rule.
👉 Example:
x = "Global"
def outer():
x = "Enclosing"
def inner():
x = "Local"
print(x)
inner()
outer()
Output:
Local
Behind the scenes, Python builds a scope chain:
inner locals → outer locals → module globals → built-ins
If it doesn’t find the name in any of those drawers? 💥 NameError
.
So when you get a NameError
, picture Python frantically running through the mansion yelling:
“I swear I checked every drawer… your socks are gone!” 🧦
🔄 Part 2: nonlocal
, Variable Borrowing
Normally, assigning inside a function creates a new local drawer.
But sometimes you don’t want a new one, you want to borrow Dad’s drawer from the next room. That’s what nonlocal
does.
👉 Example:
def outer():
message = "Hello"
def inner():
nonlocal message
message = "Hi from inner"
inner()
print(message)
outer()
Output:
Hi from inner
Without nonlocal
, Python would assume you meant a brand-new variable in inner
. With nonlocal
, you’re saying:
“No Python, use the old drawer. I’m borrowing it.”
🪢 Part 3: Closures, Time Capsules for Functions
Closures are like time capsules or backpacks 🎒. Even after a function finishes, its inner function can carry some variables with it forever.
👉 Example:
def make_multiplier(n):
def multiplier(x):
return x * n
return multiplier
times3 = make_multiplier(3)
times5 = make_multiplier(5)
print(times3(10)) # 30
print(times5(10)) # 50
Even though make_multiplier
has finished running, times3
and times5
still remember their n
.
How? Closures!
🧬 The Secret: __closure__
and cell
Objects
Closures don’t just magically “remember” variables, they literally carry them around in cells.
👉 Example:
def outer():
x = 42
def inner():
return x
return inner
func = outer()
print(func()) # 42
Let’s peek behind the curtain:
print(func.__closure__)
print(func.__closure__[0].cell_contents)
Output:
(<cell at 0x...: int object at 0x...>,)
42
What does this mean?
-
__closure__
→ A tuple of cell objects. - Each cell is like a glass jar 🫙 holding a variable.
-
cell_contents
→ The actual thing inside (here42
).
Closures are literally functions carrying jars of variables with them.
Multiple Cells Example
def outer():
a = 10
b = 20
def inner():
return a + b
return inner
func = outer()
for i, cell in enumerate(func.__closure__):
print(f"Cell {i}: {cell.cell_contents}")
Output:
Cell 0: 10
Cell 1: 20
So the closure is walking around with two jars 🫙🫙 one for a
, one for b
.
Closures Don’t Copy Values
Closures keep references, not snapshots.
def outer():
x = [1, 2, 3]
def inner():
return x
return inner
func = outer()
print(func.__closure__[0].cell_contents) # [1, 2, 3]
func.__closure__[0].cell_contents.append(4)
print(func()) # [1, 2, 3, 4]
See? The jar held the actual list, so when we changed it, the closure noticed.
This also explains why nonlocal
works: it updates the existing jar instead of creating a new one.
🧰 Real-Life Closure Magic
- Counters that remember clicks
def make_counter():
count = 0
def counter():
nonlocal count
count += 1
return count
return counter
click = make_counter()
print(click()) # 1
print(click()) # 2
- Function factories (specialized calculators).
- Data hiding (simulate private variables).
Closures = memory + functions.
🎭 Part 4: Decorators, Costume Designers for Functions
If functions are actors, decorators are the costume designers 🎭. They dress up functions with extra behavior, without changing their script.
👉 Basic Decorator:
def shout(func):
def wrapper():
return func().upper()
return wrapper
@shout
def greet():
return "hello world"
print(greet()) # HELLO WORLD
Behind the scenes, @shout
means:
greet = shout(greet)
So greet
is actually wrapper
. No black magic, just reassignment.
⏱️ Practical Decorators in Action
- Timer
import time
def timer(func):
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
end = time.time()
print(f"{func.__name__} took {end-start:.2f}s")
return result
return wrapper
@timer
def slow():
time.sleep(2)
return "Done"
slow()
- Logger (record function calls).
- Memoization (cache expensive results).
- Security checks (only allow if logged in).
- Stacked decorators (chain multiple costumes).
🔮 Advanced Wizardry: Decorator Factories & Class Decorators
Decorators can also take arguments. That’s a decorator factory.
def repeat(times):
def decorator(func):
def wrapper(*args, **kwargs):
for _ in range(times):
func(*args, **kwargs)
return wrapper
return decorator
@repeat(3)
def say_hi():
print("Hi!")
say_hi()
Executed as:
say_hi = repeat(3)(say_hi)
We can even decorate classes, transforming them when they’re created like handing Iron Man a brand-new suit 🦾.
🧠 The Big Picture
- Scopes → Python’s treasure map 🗺️ for finding variables (LEGB rule).
- Closures → Backpacks 🎒 carrying variables after their parents are gone.
-
__closure__
&cell
→ The jars 🫙 where closures actually store stuff. - Decorators → Costume designers 🎭 that rewire functions using closures.
The real superpower: functions in Python are first-class citizens. You can pass them, store them, return them, and wrap them. Everything else closures, decorators builds on that.
🎉 Final Curtain
Next time you see something like:
@something
def do_magic():
...
Remember:
- Python just swapped your function for a wrapped version.
- That wrapper probably carries jars of variables in its closure.
- And Python quietly checked the scope chain to make it all work.
That’s the real secret sauce of Python: 🪄 simple rules + flexible functions = endless magic.
Top comments (0)