Python Decorators Made Simple for Beginners
SkillVeris Team
Engineering Team

A decorator is a function that takes another function, wraps it with extra behavior, and returns the wrapped version — all without changing the original code.
In this guide, you'll learn:
- The @decorator syntax placed above a function is simply shorthand for reassigning that function to decorator(function).
- Decorators rely on the fact that in Python functions are objects you can pass around, nest, and return.
- A wrapper using *args and **kwargs can accept any arguments, so one decorator works on many different functions.
- Always use functools.wraps inside a decorator to preserve the original function's name and docstring.
1What Is a Python Decorator?
A Python decorator is a function that takes another function as input, adds some behavior around it, and returns a new wrapped function — all without modifying the original function's code. You apply one by writing @decorator_name on the line directly above a function definition.
The point is reusable, clean extension. Instead of copying the same logging, timing, or permission-checking code into dozens of functions, you write it once in a decorator and attach it wherever needed. The original function stays focused on its real job while the decorator handles the surrounding concern.
2First, Functions Are Objects
Decorators only make sense once you accept one idea: in Python, functions are ordinary objects. You can assign them to variables, pass them as arguments, and return them from other functions.
- Assign: greet = say_hello # now greet() runs say_hello()
- Pass: apply(say_hello) # a function taking a function
- Return: a function can create and return another function
- Nest: you can define a function inside another function
🔑The Core Insight
Because functions can be passed around and returned like any value, you can write a function whose whole job is to wrap and enhance another function.
3Building a Decorator by Hand
Before the @ syntax, it helps to build a decorator manually so you see exactly what happens. A decorator defines an inner wrapper, then returns it.
The Decorator
This decorator prints a message before and after the function it wraps, then returns the inner wrapper function instead of calling it.
def announce(func):
def wrapper():
print('Before the function runs')
func()
print('After the function runs')
return wrapperApplying It Manually
You can attach it without any special syntax by reassigning the name. This line is exactly what the @ symbol automates.
def say_hi():
print('Hi!')
say_hi = announce(say_hi) # wrap it
say_hi() # now prints before, Hi!, and after4The @ Syntax Explained
The @decorator syntax is pure shorthand for that manual reassignment. Placing @announce above a function definition does the wrapping automatically.
In other words, writing @announce above def say_hi(): is identical to defining say_hi normally and then writing say_hi = announce(say_hi). The @ symbol just makes the intent clear and keeps the wrapping right next to the definition where it belongs. Once you see it as reassignment, the magic disappears.
- @announce
- def say_hi():
- print('Hi!')
- # say_hi is now the wrapped version automatically
5Handling Any Arguments
The simple wrapper above breaks if the wrapped function takes arguments. The fix is to make the wrapper accept anything and pass it straight through.
- def announce(func):
- def wrapper(*args, **kwargs):
- print('Before')
- result = func(*args, **kwargs)
- print('After')
- return result
- return wrapper
💡Always Return the Result
Capture the wrapped call in result and return it. Forgetting to return means decorated functions silently give back None — a classic beginner bug.
6Preserve the Function's Identity
There is one subtle problem: after wrapping, the decorated function reports the wrapper's name and loses its docstring. The standard library fixes this cleanly.
Apply @functools.wraps(func) to your inner wrapper. It copies the original function's name, docstring, and metadata onto the wrapper so tools, debuggers, and help() still see the real function. It is a one-line habit that every well-written decorator follows.
- from functools import wraps
- def announce(func):
- @wraps(func)
- def wrapper(*args, **kwargs):
- return func(*args, **kwargs)
- return wrapper
7Real-World Uses
Decorators are everywhere in real Python code because they cleanly separate a cross-cutting concern from core logic.
- Logging: record when a function is called and with what arguments.
- Timing: measure and print how long a function takes to run.
- Caching: store results so repeated calls skip recomputation (functools.lru_cache).
- Access control: check permissions before a view function runs.
- Retrying: automatically re-run a function that failed on a transient error.
🔑You Already Use Them
Frameworks lean on decorators heavily — think @app.route in Flask or @property in classes. Learning to read them makes a huge amount of Python code clearer.
8Common Mistakes to Avoid
A few recurring errors trip up people writing their first decorators.
- Forgetting *args and **kwargs, so the decorator only works on argument-less functions.
- Not returning the wrapped function's result, so calls return None.
- Skipping functools.wraps, which hides the real function's name and docstring.
- Calling the function instead of returning the wrapper (return wrapper, not wrapper()).
- Over-decorating simple functions where the added indirection is not worth it.
⚠️Watch Out
return wrapper returns the function; return wrapper() calls it immediately and returns its result. That single pair of parentheses is a very common and confusing bug.
9Key Takeaways
Decorators feel like magic until you see the simple mechanics underneath.
- A decorator wraps a function to add behavior without changing its code.
- @decorator is shorthand for function = decorator(function).
- They work because functions are objects you can pass and return.
- Use *args and **kwargs so one decorator works on any function.
- Always add functools.wraps to preserve the original name and docstring.
10Frequently Asked Questions
Q: What is a decorator in simple terms? A: A decorator is a function that wraps another function to add extra behavior, like logging or timing, without changing the original function's code. You attach it by writing @decorator_name above a function, which is just shorthand for reassigning the function to its wrapped version.
Q: What does the @ symbol actually do? A: The @ symbol applies a decorator. Writing @announce above def foo() is exactly equivalent to defining foo and then writing foo = announce(foo). It automates the reassignment and keeps the wrapping visible right above the function definition.
Q: Why should I use functools.wraps? A: Without it, a decorated function reports the wrapper's name and loses its docstring, which confuses debuggers, documentation tools, and help(). Adding @functools.wraps(func) to the inner wrapper copies the original function's identity across so everything still works as expected.
Q: Can a decorator accept its own arguments? A: Yes, but it requires an extra layer: a function that takes the arguments and returns an actual decorator, which then wraps the target function. This three-level pattern is common for things like @retry(times=3), and it builds naturally on the basic decorator you already understand.
Related Reading
Get The Print Version
Download a PDF of this article for offline reading.
About the Publisher
SkillVeris Team
Engineering Team
Our engineering writers turn abstract code concepts into hands-on, project-driven learning experiences.
View all postsRelated Posts
Never miss an update
Get the latest tutorials and guides delivered to your inbox.
No spam. Unsubscribe anytime.