🧙 Scopes, Closures, and Decorators in Python: A Fun Deep Dive with Magic Behind the Scenes

Anik SikderAnik Sikder
4 min read

Imagine Python as Hogwarts. Variables are spells, functions are wizards, and decorators are those cool magical gadgets Fred & George sell at the joke shop. To truly master the magic, you must understand three things: Scopes, Closures, and Decorators.
Grab your butterbeer, this is going to be fun 🍻.

🎭 The Stage: What are Scopes?

In Python, scope is like a stage where variables live.
Think of it like this:

  • Global scope → The big wide world 🌍 (anyone can see you).

  • Local scope → Your room 🛏️ (only you can see what’s inside).

  • Enclosing scope → Your parents’ house 🏠 (you don’t own it, but you can access it).

  • Built-in scope → The universe 🌌 (Python’s gods decided len, print, etc. just exist).

👉 Python follows the LEGB Rule (Local → Enclosing → Global → Built-in) to decide where to find a variable.

Example:

x = "global 🌍"

def outer():
    x = "outer 🏠"
    def inner():
        x = "local 🛏️"
        print(x)
    inner()

outer()  # Output: local 🛏️

Why? Because Python looks inside → parent → global → built-in until it finds the variable. If not found… you get the dreaded NameError.

🔮 Behind the Scenes: __globals__ and __builtins__

Every function in Python carries a backpack 🎒 of context.

  • __globals__ → Points to the global variables it can see.

  • __builtins__ → The universal spells (len, print, etc.).

def magic():
    pass

print(magic.__globals__.keys())  # shows global stuff

This is Python’s way of keeping track of "who lives where."

🌀 Enter Closures: Functions That Remember

A closure is when an inner function remembers variables from its enclosing scope, even if the outer function is gone.
It’s like your grandma’s recipe 🍲 she’s not in the kitchen anymore, but you still have access to her secret ingredients.

Example:

def make_multiplier(factor):
    def multiply(number):
        return number * factor
    return multiply

double = make_multiplier(2)
print(double(10))  # 20

Even though make_multiplier is done executing, multiply remembers the factor.

🧩 Behind the Scenes: The __closure__ & __cell__

Now the real magic 🔥.

print(double.__closure__)

Output:

(<cell at 0x...: int object at 0x...>,)

That cell object (__cell__) is Python’s horcrux 🧙‍♂️ it holds the preserved value from the outer function. Without it, closures wouldn’t exist.

You can even peek inside:

print(double.__closure__[0].cell_contents)  # 2

So closures aren’t just “functions remembering stuff.”
They literally carry tiny memory cells (__cell__) with them.

🎭 Real-Life Closure Example: Password Protector

def password_protector(secret):
    def get_password():
        return secret
    return get_password

locker = password_protector("🔑 swordfish")
print(locker())  # 🔑 swordfish

The variable secret lives on, tucked safely inside a closure cell. Even though password_protector is long gone, the inner function clings to it like a diary.

🎁 Enter Decorators: Functions That Wrap Functions

If closures are memory keepers, decorators are fashion stylists 👗. They take a function, dress it up with new abilities, and send it back on stage.

Example:

def greet(func):
    def wrapper():
        print("👋 Hello, traveler!")
        func()
        print("🎉 Goodbye!")
    return wrapper

@greet
def say_name():
    print("I am Anik 🧑‍💻")

say_name()

Output:

👋 Hello, traveler!
I am Anik 🧑‍💻
🎉 Goodbye!

That @greet line is just Python sugar 🍭 for:

say_name = greet(say_name)

⏱️ Decorator Applications in Real Life

  1. Timer Decorator
    Measure how long a function takes:
import time

def timer(func):
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        end = time.time()
        print(f"⏱️ Took {end-start:.2f}s")
        return result
    return wrapper

@timer
def slow_function():
    time.sleep(2)

slow_function()
  1. Logger Decorator 📜
    Track what’s happening:
def logger(func):
    def wrapper(*args, **kwargs):
        print(f"🚀 Calling {func.__name__} with {args} {kwargs}")
        return func(*args, **kwargs)
    return wrapper
  1. Memoization 🧠
    Remember results so Python doesn’t repeat hard work:
def memoize(func):
    cache = {}
    def wrapper(x):
        if x not in cache:
            cache[x] = func(x)
        return cache[x]
    return wrapper

🧪 Behind the Scenes of Decorators

  • Decorators rely on closures.

  • The wrapper function holds onto func via a closure cell (__cell__).

  • When you call say_name, you’re actually calling wrapper.

👉 This means scopes → closures → decorators is a natural progression.
They’re not separate ideas, but layers of the same onion 🧅.

🏆 Final Thoughts

  • Scopes decide “who can see what.”

  • Closures let functions remember things, thanks to __closure__ and __cell__.

  • Decorators let us wrap functions with superpowers.

Next time you write Python, remember: you’re not just coding, you’re performing magic ✨.

💡 Pro tip: Peek inside your functions with __globals__, __closure__, and __cell_contents. It feels like hacking into Python’s brain 🧠.

0
Subscribe to my newsletter

Read articles from Anik Sikder directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Anik Sikder
Anik Sikder

Full-Stack Developer & Tech Writer specializing in Python (Django, FastAPI, Flask) and JavaScript (React, Next.js, Node.js). I build fast, scalable web apps and share practical insights on backend architecture, frontend performance, APIs, and Web3 integration. Available for freelance and remote roles.