Question
Python Function Decorators: How to Create and Chain Them Together
Question
In Python, how can I create two decorators so they can be stacked like this?
@make_bold
@make_italic
def say():
return "Hello"
When say() is called, it should return:
"<b><i>Hello</i></b>"
I want to understand how to define these decorators and how decorator chaining works when multiple decorators are applied to the same function.
Short Answer
By the end of this page, you will understand what Python decorators are, how to write simple decorators that wrap a function, and how stacked decorators are applied in order. You will also see how to trace execution step by step, avoid common mistakes, and use decorators in practical code.
Concept
Decorators in Python are functions that take another function, extend or modify its behavior, and return a new function.
In this example, the goal is not to change the original say() function directly. Instead, each decorator wraps it with extra behavior:
make_italicadds<i>...</i>around the returned stringmake_boldadds<b>...</b>around the returned string
A decorator works because functions in Python are first-class objects. That means you can:
- pass functions as arguments
- return functions from other functions
- assign functions to variables
When decorators are stacked, Python applies them from the bottom up.
@make_bold
@make_italic
def say():
return "Hello"
This is equivalent to:
def say():
return "Hello"
say = make_bold(make_italic(say))
So the execution flow is:
say()returns
Mental Model
Think of a decorator like gift wrapping.
- The original function is the item inside the box.
- One decorator adds one layer of wrapping.
- Another decorator adds another layer around that.
If say() returns Hello, then:
make_italicwraps it first:<i>Hello</i>make_boldwraps the result again:<b><i>Hello</i></b>
So stacked decorators are like nested layers around the original function result.
Syntax and Examples
A basic decorator has this shape:
def my_decorator(func):
def wrapper():
return func()
return wrapper
For your example, you can write:
def make_bold(func):
def wrapper():
return f"<b>{func()}</b>"
return wrapper
def make_italic(func):
def wrapper():
return f"<i>{func()}</i>"
return wrapper
@make_bold
@make_italic
def say():
return "Hello"
print(say())
Output:
<b><i>Hello</i></b>
How it works
- receives and returns a new
Step by Step Execution
Consider this code:
def make_bold(func):
def wrapper():
return f"<b>{func()}</b>"
return wrapper
def make_italic(func):
def wrapper():
return f"<i>{func()}</i>"
return wrapper
@make_bold
@make_italic
def say():
return "Hello"
This is processed like this:
def say():
return "Hello"
say = make_bold(make_italic(say))
Step-by-step when Python defines say
- Python creates the original
sayfunction. - Python applies
make_italic(say). make_italicreturns its function.
Real World Use Cases
Decorators are often used when you want to add the same behavior to many functions without repeating code.
Formatting output
Your HTML example is a formatting use case:
- wrapping strings in tags
- adding prefixes or suffixes
- converting output to a standard format
Logging
def log_call(func):
def wrapper():
print(f"Calling {func.__name__}")
return func()
return wrapper
Used for:
- debugging
- auditing
- tracking execution
Authentication and permissions
In web apps, decorators often check whether a user is allowed to run a function.
def require_admin(func):
def wrapper(user):
if not user.get("is_admin"):
return "Access denied"
return func(user)
return wrapper
Real Codebase Usage
In real projects, decorators are usually written to handle any arguments, not just functions with no parameters.
A common pattern is:
from functools import wraps
def my_decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
# extra behavior before
result = func(*args, **kwargs)
# extra behavior after
return result
return wrapper
Why *args and **kwargs matter
They allow the decorator to work with many different functions:
- functions with positional arguments
- functions with keyword arguments
- functions with optional parameters
Common patterns in real codebases
- Guard clauses: stop execution early if conditions are not met
- Validation: check argument values before calling the function
- Error handling: catch exceptions and return safer output
- Logging: record when a function starts and finishes
- Configuration: apply environment-specific behavior
- Caching: reuse previous results for expensive calls
Example with arguments
Common Mistakes
1. Forgetting to return the wrapper
Broken code:
def make_bold(func):
def wrapper():
return f"<b>{func()}</b>"
Problem:
- The decorator returns
None - The decorated function becomes unusable
Fix:
def make_bold(func):
def wrapper():
return f"<b>{func()}</b>"
return wrapper
2. Returning the function instead of calling it
Broken code:
def make_italic(func):
def wrapper():
return f"<i>{func}</i>"
return wrapper
Problem:
Comparisons
| Concept | What it does | Best use case | Example |
|---|---|---|---|
| Decorator | Wraps a function with reusable behavior | Add logic before/after function calls | Logging, formatting, auth |
| Regular helper function | Performs a separate task directly | Simple one-off processing | format_html(text) |
| Higher-order function | Takes or returns functions | Functional patterns in general | map, callbacks, decorators |
| Wrapper function | A function around another function | Basis of decorators | Internal wrapper() |
Decorator vs calling helper functions manually
Without decorators:
Cheat Sheet
Basic decorator pattern
def decorator(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
String-wrapping decorators
from functools import wraps
def make_bold(func):
@wraps(func)
def wrapper(*args, **kwargs):
return f"<b>{func(*args, **kwargs)}</b>"
return wrapper
def make_italic(func):
@wraps(func)
def wrapper(*args, **kwargs):
return f"<i>{func(*args, **kwargs)}</i>"
return wrapper
Stacking order
@a
():
FAQ
How do Python decorators work?
A decorator is a function that takes another function, wraps it with extra behavior, and returns the wrapped version.
In what order are stacked decorators applied?
They are applied from the bottom up. @a above @b means a(b(func)).
Why should I use functools.wraps?
It preserves the original function's name, docstring, and other metadata, which helps with debugging and tools.
Can decorators accept function arguments?
Yes. The wrapper should usually use *args and **kwargs so it can pass arguments through.
Can a decorator change the return value?
Yes. In your example, the decorators change the returned string by wrapping it in HTML tags.
Do decorators only work for formatting strings?
No. They are commonly used for logging, validation, caching, permissions, and error handling.
What is the difference between a decorator and a normal function?
A normal function performs a task directly. A decorator modifies or extends another function's behavior.
Can I stack more than two decorators?
Yes. You can stack multiple decorators, and each one wraps the result of the one below it.
Mini Project
Description
Build a small text-formatting utility using Python decorators. The project demonstrates how decorators can transform function output and how stacking changes the final result. This is useful for understanding not just HTML-style wrapping, but the broader idea of composing reusable behavior.
Goal
Create reusable decorators that wrap text output and apply multiple formatting layers to a function.
Requirements
- Create a decorator that wraps returned text in
<b>tags. - Create a decorator that wraps returned text in
<i>tags. - Make both decorators work with functions that accept arguments.
- Stack the decorators on at least one function.
- Print the final result to confirm the order of wrapping.
Keep learning
Related questions
@staticmethod vs @classmethod in Python Explained
Learn the difference between @staticmethod and @classmethod in Python with clear examples, use cases, mistakes, and a mini project.
Catch Multiple Exceptions in One except Block in Python
Learn how to catch multiple exceptions in one Python except block using tuples, with examples, mistakes, and real-world usage.
Convert Bytes to String in Python 3
Learn how to convert bytes to str in Python 3 using decode(), text mode, and proper encodings with practical examples.