Python Decorators
Learn function decorators, wrapper behavior, functools.wraps, decorator factories and application order, plus how class decorators transform class bindings.
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.
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.
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.
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.
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.
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.
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.
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.
Output
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.
Thanks for your feedback.
Mark this learning resource complete to track your learning progress.