Skip to content
Foxipa

Suggest an edit

Tell us what should be corrected, clarified, or improved. Submission will be connected to Foxipa’s contribution service later.

No information is sent anywhere yet. Your suggestion is only prepared in your browser.

Learning

Python Decorators

Learn function decorators, wrapper behavior, functools.wraps, decorator factories and application order, plus how class decorators transform class bindings.

Levels: Intermediate ~28 min read

Intermediate

A decorator is a function that takes a function and returns a callable replacement or wrapper. Python lets you apply a decorator to a function with @ syntax, so behavior can be added or changed without rewriting the function body.

A decorator takes a function

A decorator is an ordinary callable. When a function is decorated, Python passes the function object to the decorator and binds the returned value to the original function name.

python
def add_greeting(func):
    def wrapper(name):
        return "Hello, " + func(name)
    return wrapper

def greet(name):
    return name

greet = add_greeting(greet)
print(greet("Ada"))

The assignment at the end shows the mechanism directly: the original function is passed to the decorator, and the returned wrapper becomes the value bound to greet.

@ applies the decorator at definition time

The @decorator form is a shorter way to apply a decorator when the function is defined. The decorator expression is evaluated, then the resulting callable is invoked with the new function object.

python
def add_greeting(func):
    def wrapper(name):
        return "Hello, " + func(name)
    return wrapper

@add_greeting
def greet(name):
    return name

print(greet("Ada"))

A wrapper can add behavior

A common decorator returns a wrapper that performs work before or after calling the original function. The wrapper can receive the call arguments and forward them to the original function.

python
def announce(func):
    def wrapper(*args, **kwargs):
        print("Calling", func.__name__)
        result = func(*args, **kwargs)
        print("Finished")
        return result
    return wrapper

@announce
def add(left, right):
    return left + right

print(add(3, 4))

Decorators can change the returned value

A decorator does not have to print or log. Its wrapper can change what callers receive, while still using the original function to perform the underlying work.

python
def double_result(func):
    def wrapper(value):
        return func(value) * 2
    return wrapper

@double_result
def square(value):
    return value * value

print(square(3))

Decorator expressions can take arguments

Sometimes the decorator itself needs configuration. In that case, a call such as @prefix("VIP") first produces a decorator. That decorator then receives the function being defined.

python
def prefix(label):
    def decorate(func):
        def wrapper(name):
            return f"{label}: {func(name)}"
        return wrapper
    return decorate

@prefix("VIP")
def describe(name):
    return name

print(describe("Ada"))

Multiple decorators are applied from the inside out

When several decorators appear above one function, the closest decorator is applied first. The resulting callable is then passed to the next decorator. This is equivalent to nested function calls.

python
def add_exclamation(func):
    def wrapper(text):
        return func(text) + "!"
    return wrapper

def add_brackets(func):
    def wrapper(text):
        return "[" + func(text) + "]"
    return wrapper

@add_brackets
@add_exclamation
def message(text):
    return text

print(message("hello"))

Preserve function metadata with functools.wraps

A wrapper is a new function, so without extra help its name and documentation describe the wrapper rather than the function being decorated. Apply functools.wraps(func) to the wrapper definition to copy the usual identifying metadata and attach a __wrapped__ reference to the original function. This keeps tools such as inspect.signature() able to follow the wrapper chain when showing the original callable signature; it does not change which arguments the wrapper code itself accepts.

python
from functools import wraps
from inspect import signature

def traced(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print("Calling", func.__name__)
        return func(*args, **kwargs)
    return wrapper

@traced
def greet(name: str, punctuation: str = "!") -> str:
    """Return a greeting for name."""
    return f"Hello, {name}{punctuation}"

print(greet.__name__)
print(greet.__doc__)
print(signature(greet))
print(greet("Ada"))
print(greet.__wrapped__("Ada", "."))

The wrapper still receives *args and **kwargs, but wraps preserves the original function’s usual metadata and exposes the original through __wrapped__. That distinction matters for help(), debugging, and introspection: metadata preservation improves how the decorated callable is presented without rewriting the wrapper’s implementation.

Decorate a class

A class decorator is applied after the class object has been created. Python passes that class object to the decorator, then binds the decorator’s returned value to the class name. A decorator may add attributes, register the class, or return a replacement; if you want the original class to remain available under its name, return the class object.

Interactive Runnable example
Edit

Ready to run.

This is related to function decorators, but the decorated object is a class rather than a function. For example, @dataclass is a class decorator that adds generated methods. The details of dataclasses belong to the separate Dataclasses lesson; here, focus on the general class-definition rule.

Quick Check: keep the class binding

Which code belongs in the decorator so the class name still refers to the class after @set_category runs?

def set_category(cls):
    cls.category = "report"
    

@set_category
class Summary:
    pass

print(type(Summary).__name__)

Choose the right level of abstraction

Decorators are useful when the same kind of behavior should be applied around multiple functions: logging, validation, timing, access checks, or other cross-cutting behavior. For one function, ordinary code is often easier to read. A decorator should make the repeated design clearer, not hide important control flow.

Decorators are part of Python function-definition syntax, but they do not create a special kind of function. The decorator is simply a callable transformation performed when the definition is executed. Class decorators and advanced decorator metadata are separate topics and are not required to understand function decorators.

Key takeaways

  • A decorator receives a function and returns a callable.
  • @decorator applies the decorator when the function definition executes.
  • A wrapper can add behavior before or after calling the original function.
  • Decorator factories let you configure a decorator with arguments.
  • Multiple decorators are applied from the closest decorator outward.
  • Use functools.wraps(func) so a wrapper keeps the decorated function’s identifying metadata and exposes __wrapped__ for introspection.
Was this page helpful?

Mark this learning resource complete to track your learning progress.