A gift and its wrapping paper are two different things, but once wrapped, you interact with the whole package as one. You unwrap it, and the original gift is still there underneath, untouched, just presented differently. That's precisely what a decorator does to a function in Python: it wraps it with extra behavior, without changing the original function itself.
Step One: Understanding Closures
Before decorators make sense, you need to understand closures, since decorators are built on top of them.
A closure is a function that remembers variables from the place it was created, even after that outer function has finished running.
def make_greeter(name): def greet(): print(f"Hello, {name}!") return greet say_hi_to_alex = make_greeter("Alex")say_hi_to_alex() # Hello, Alex!Notice that make_greeter() already finished running by the time say_hi_to_alex() is called. Yet it still remembers name. That's the closure: the inner function "closes over" the variable from its birthplace and carries it along, like a gift box that remembers what's inside it even after it leaves the workshop.
Step Two: A Function That Wraps Another Function
Now let's build something that takes a function as an ingredient and wraps extra behavior around it, exactly like wrapping paper around a gift.
def add_wrapping(func): def wrapped(): print("🎁 Wrapping paper on") func() print("🎁 Wrapping paper off") return wrapped def give_gift(): print("Here is your gift!") wrapped_gift = add_wrapping(give_gift)wrapped_gift()Output:
The original give_gift() function never changed. add_wrapping() just built something new around it.
Step Three: The @decorator Shortcut
Writing wrapped_gift = add_wrapping(give_gift) every time is repetitive. Python gives you @ syntax to apply this wrapping automatically.
def add_wrapping(func): def wrapped(): print("🎁 Wrapping paper on") func() print("🎁 Wrapping paper off") return wrapped @add_wrappingdef give_gift(): print("Here is your gift!") give_gift()@add_wrapping above def give_gift() means exactly the same thing as the manual version, it's just cleaner to read. This is a decorator: a function that wraps another function to add behavior before or after it runs, without touching its original code.
Handling Functions With Arguments
Real gifts come in different shapes and sizes, and real functions take arguments. Use *args and **kwargs so your decorator works on any function, no matter what it accepts.
Know about *args and **kwargs:
Part 3: *args and **kwargs in Python Explained Like a Potluck Party
def add_wrapping(func): def wrapped(*args, **kwargs): print("🎁 Wrapping paper on") result = func(*args, **kwargs) print("🎁 Wrapping paper off") return result return wrapped @add_wrappingdef give_gift(name): print(f"Here is your gift, {name}!") give_gift("Sam")A Real-World Example: Timing a Function
Decorators aren't just a teaching toy, they're everywhere in real Python code. Here's one that measures how long a function takes to run.
This is exactly the same pattern used by real frameworks: Flask's @app.route, for example, wraps your function to connect it to a web URL, without changing what your function itself does.
What's Next
That wraps up the core series on Python functions, from writing your first def to wrapping functions with decorators. With parameters, *args/**kwargs, lambdas, scope, recursion, and decorators covered, you now have every building block needed to read and write real-world Python code with confidence.
Python Functions 7-Part Masterclass:
- Part 1: Python Functions Explained Like a Recipe (Beginner's Guide)
- Part 2: Python Function Parameters and Arguments (Explained Like Ordering Food)
- Part 3: *args and **kwargs in Python Explained Like a Potluck Party
- Part 4: Python Lambda Functions Explained Like a Sticky Note
- Part 5: Python Variable Scope Explained Like an Office Building
- Part 6: Python Recursion Explained Like Russian Nesting Dolls
- Part 7: Python Closures and Decorators Explained Like Gift Wrapping (Current)