Question

Python Function Decorators: How to Create and Chain Them Together

pythonfunctiondecoratorpython-decoratorschain

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_italic adds <i>...</i> around the returned string
  • make_bold adds <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:

  1. say() returns "Hello"
  2. make_italic wraps it into "<i>Hello</i>"
  3. make_bold wraps that into "<b><i>Hello</i></b>"

Decorators matter because they let you reuse behavior cleanly. Instead of repeating the same logic in many functions, you can write it once and apply it wherever needed.

Common real uses include:

  • logging
  • access control
  • validation
  • caching
  • timing
  • formatting output

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_italic wraps it first: <i>Hello</i>
  • make_bold wraps the result again: <b><i>Hello</i></b>

So stacked decorators are like nested layers around the original function result.

Take Quiz

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

  • make_italic receives say and returns a new wrapper
  • make_bold receives that new wrapped function and returns another wrapper
  • Calling say() actually calls the outermost wrapper

A better version with functools.wraps

In real code, use functools.wraps to preserve the original function's name and metadata.

from functools import wraps


def make_bold(func):
    @wraps(func)
    def wrapper():
        return f"<b>{func()}</b>"
    return wrapper


def make_italic(func):
    @wraps(func)
    def wrapper():
        return f"<i>{func()}</i>"
    return wrapper


@make_bold
@make_italic
def say():
    return "Hello"

print(say())

This gives the same output, but is safer for debugging and tooling.

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

  1. Python creates the original say function.
  2. Python applies make_italic(say).
  3. make_italic returns its wrapper function.
  4. Python applies make_bold(...) to that returned wrapper.
  5. make_bold returns another wrapper.
  6. The name say now refers to the outer wrapper, not the original function directly.

Step-by-step when say() is called

  1. The outer make_bold wrapper runs.
  2. Inside it, func() is called.
  3. That func() is actually the make_italic wrapper.
  4. The italic wrapper calls the original say().
  5. Original say() returns "Hello".
  6. Italic wrapper returns "<i>Hello</i>".
  7. Bold wrapper receives that and returns "<b><i>Hello</i></b>".

Trace version

def make_bold(func):
    def wrapper():
        value = func()
        print("bold received:", value)
        return f"<b>{value}</b>"
    return wrapper


def make_italic(func):
    def wrapper():
        value = func()
        print("italic received:", value)
        return f"<i>{value}</i>"
    return wrapper


@make_bold
@make_italic
def say():
    return "Hello"

print(say())

Possible output:

italic received: Hello
bold received: <i>Hello</i>
<b><i>Hello</i></b>

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

Timing and performance measurement

Decorators can measure how long a function takes.

Caching

A decorator can store previous results and reuse them to avoid repeated work.

Validation

Decorators can check inputs before allowing a function to continue.

Take Quiz

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

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


@make_bold
@make_italic
def greet(name):
    return f"Hello, {name}"

print(greet("Sam"))

Output:

<b><i>Hello, Sam</i></b>

This version is much more useful in production code.

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:

  • func is a function object
  • func() calls the function and gets its result

Fix:

def make_italic(func):
    def wrapper():
        return f"<i>{func()}</i>"
    return wrapper

3. Not supporting arguments

Broken code:

def make_bold(func):
    def wrapper():
        return f"<b>{func()}</b>"
    return wrapper

@make_bold
def greet(name):
    return name

Problem:

  • greet("Ava") will fail because wrapper() accepts no arguments

Fix:

def make_bold(func):
    def wrapper(*args, **kwargs):
        return f"<b>{func(*args, **kwargs)}</b>"
    return wrapper

4. Confusing decorator order

@make_bold
@make_italic
def say():
    return "Hello"

This means:

say = make_bold(make_italic(say))

The bottom decorator is applied first.

5. Forgetting functools.wraps

Without @wraps(func), metadata such as __name__ and docstrings may be lost.

Better version:

from functools import wraps


def make_bold(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        return f"<b>{func(*args, **kwargs)}</b>"
    return wrapper

6. Assuming decorators only work with strings

Your example wraps strings, but decorators can also:

  • check permissions
  • log calls
  • cache results
  • validate inputs
  • retry failed operations

The decorator idea is broader than text formatting.

Comparisons

ConceptWhat it doesBest use caseExample
DecoratorWraps a function with reusable behaviorAdd logic before/after function callsLogging, formatting, auth
Regular helper functionPerforms a separate task directlySimple one-off processingformat_html(text)
Higher-order functionTakes or returns functionsFunctional patterns in generalmap, callbacks, decorators
Wrapper functionA function around another functionBasis of decoratorsInternal wrapper()

Decorator vs calling helper functions manually

Without decorators:

def say():
    return "Hello"

text = say()
text = make_italic_text(text)
text = make_bold_text(text)

With decorators:

@make_bold
@make_italic
def say():
    return "Hello"

Decorators are better when:

  • the pattern should be reusable
  • many functions need the same behavior
  • you want behavior attached directly to function definitions

Stacked decorator order

Syntax orderApplication orderResult order
@make_bold then @make_italicmake_italic first, then make_bolditalic inside bold
@make_italic then @make_boldmake_bold first, then make_italicbold inside italic
Take Quiz

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
@b
def f():
    pass

Equivalent to:

f = a(b(f))

Rules to remember

  • A decorator takes a function as input
  • It usually returns a new wrapper function
  • The wrapper often calls the original function
  • Use *args, **kwargs for flexibility
  • Use functools.wraps to preserve metadata
  • Bottom decorator is applied first

Common edge case

If your wrapper does not accept arguments, decorated functions with parameters will fail.

Expected result for this question

@make_bold
@make_italic
def say():
    return "Hello"

say()
# '<b><i>Hello</i></b>'

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.

Take Quiz

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

  1. Create a decorator that wraps returned text in <b> tags.
  2. Create a decorator that wraps returned text in <i> tags.
  3. Make both decorators work with functions that accept arguments.
  4. Stack the decorators on at least one function.
  5. Print the final result to confirm the order of wrapping.
Take Quiz

Keep learning

Related questions

View more