Maintenance

Site is under maintenance — quizzes are still available.

Go to quizzes

Python Function Decorators

Learn to build and use decorators for functions in Python: understand the wrapper pattern, add behavior without modifying the original function, and apply decorators to logging, timing, and authentication. Includes a hands-on exercise and common troubleshooting.

Focus: build and use decorators for functions

Sponsored

You've written a function that does exactly what you need — say you've got a get_user_data() that fetches from an API — but now you need to log every call, or measure how long it takes, or check permissions before it runs. If you copy-paste the same logging code into every function, your codebase becomes brittle and noisy. Decorators solve that: they let you wrap a function with reusable behavior, adding capabilities without touching the original function's body. By the end of this lesson you'll build your own decorators, understand the @ syntax, and know how to apply them to real-world tasks like timing, logging, and access control.

The problem this lesson solves

When a project grows, you inevitably need to run the same logic before or after many functions: print a timestamp, verify a user is logged in, retry on failure, or cache results. The naive approach — adding the same three lines of print or if to every function — leads to:

  • Code duplication: The same pattern lives in dozens of places.
  • Maintenance pain: Change the logging format? You now have to update every function.
  • Violation of DRY: You're repeating yourself across the codebase.

Pro tip: Duplication is the #1 smell that tells you “this needs a decorator.” If you see the same three lines at the start of five functions, that's a candidate for a decorator.

Without decorators, you might reach for a helper function that wraps the call — but the calling code still needs to manually call that wrapper. Decorators make the extra behavior invisible to the call site: you just put @log_me above your function, and it automatically gets the new superpower.

Core concept / mental model

Think of a decorator as a function that takes another function and returns a new function — one that often adds something before and/or after the original call.

Here's the mental model in three layers:

  1. The original function — your do_work() that does the core job.
  2. The wrapper function — a new function that runs extra code, calls the original, and may run more extra code afterward.
  3. The decorator factory — the outer function that receives the original and returns the wrapper.

A diagram in words:

@my_decorator
def greet(name):
    print(f"Hello, {name}!")

Is equivalent to writing:

greet = my_decorator(greet)

Key insight: @my_decorator is just syntactic sugar. Python calls my_decorator(greet), and the return value (a new function) gets bound to the name greet. From then on, every call to greet() actually calls the wrapper that my_decorator returned.

Core definitions

  • Decorator: A callable that accepts a function (or class) as an argument and returns a replacement.
  • Wrapper: The inner function that the decorator defines; it contains the extra logic and calls the original.
  • @ syntax: Placing @decorator_name above a function definition applies the decorator at definition time.

How it works step by step

Let's build a minimal decorator from scratch so you see every moving part.

Step 1: Write the outer (decorator) function

def my_decorator(func):
    def wrapper():
        print("Before the function runs.")
        func()
        print("After the function runs.")
    return wrapper

my_decorator takes func (the original function). Inside, wrapper is defined — it adds behavior before and after calling func. Finally, wrapper is returned without being called.

Step 2: Decorate a function

def say_hello():
    print("Hello!")

say_hello = my_decorator(say_hello)

Now say_hello is a reference to wrapper. When you call say_hello(), you actually call wrapper(), which prints the extra lines and then runs the original say_hello.

Step 3: Use the @ syntax (preferred)

@my_decorator
def say_hello():
    print("Hello!")

This is exactly the same as step 2 — Python does the assignment for you.

Step 4: Calling the decorated function

say_hello()

Output:

Before the function runs.
Hello!
After the function runs.

Pro tip: The wrapper replaces your original function. If you want to preserve the original's metadata (name, docstring, etc.), use functools.wraps — we'll cover that in the troubleshooting section.

Hands-on walkthrough

Let's apply decorators to three practical scenarios: timing, logging, and authentication. Each example is complete and runnable.

Example 1: Timing decorator

import time

def timer(func):
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = func(*args, **kwargs)
        end = time.perf_counter()
        print(f"{func.__name__} took {end - start:.4f} seconds")
        return result
    return wrapper

@timer
def slow_function():
    total = sum(range(1000000))
    return total

result = slow_function()
# output (approx): slow_function took 0.0345 seconds
print(f"Result: {result}")  # Result: 499999500000

Note how the wrapper accepts *args and **kwargs so it can pass them through to the decorated function — this is essential so that decorators work with functions that take arguments.

Example 2: Logging decorator

def logger(func):
    def wrapper(*args, **kwargs):
        print(f"Calling {func.__name__} with args={args}, kwargs={kwargs}")
        result = func(*args, **kwargs)
        print(f"{func.__name__} returned {result}")
        return result
    return wrapper

@logger
def add(a, b):
    return a + b

add(3, 5)
# output:
# Calling add with args=(3, 5), kwargs={}
# add returned 8

Example 3: Authentication decorator (simulated)

def require_auth(func):
    def wrapper(user, *args, **kwargs):
        if not user.get("is_authenticated", False):
            print("Access denied: user not authenticated.")
            return None
        print(f"User {user['name']} authenticated — proceeding.")
        return func(user, *args, **kwargs)
    return wrapper

@require_auth
def view_dashboard(user):
    print(f"Welcome to the dashboard, {user['name']}!")

# Authenticated user
admin = {"name": "Alice", "is_authenticated": True}
view_dashboard(admin)
# output:
# User Alice authenticated — proceeding.
# Welcome to the dashboard, Alice!

# Unauthenticated user
bob = {"name": "Bob", "is_authenticated": False}
view_dashboard(bob)
# output:
# Access denied: user not authenticated.

Pro tip: When your decorator needs to check something about the arguments (like user), make the wrapper accept the same parameters as the original, or use *args, **kwargs and inspect them.

Compare options / when to choose what

Not all ways to add cross-cutting behavior are equal. Here's a comparison table:

Approach When to use Pros Cons
Plain decorator (this lesson) Adding behavior before/after many functions DRY, clean syntax, composable Must understand closures; can lose function metadata
Decorator with arguments (e.g. @log(level="INFO")) When the decorator needs configuration Flexible, parameterized More complicated to write (3 levels of nesting)
Context manager (with statement) Setup/teardown around a single block, not per-function Clean scoping, exception safe Not reusable across functions without a wrapper
Functor / callable class When state needs to be shared across calls Thread-safe state, reusable Verbose; less intuitive

For most cases — logging, timing, caching, auth — a plain decorator is the right tool. If you need to pass options (like log level), reach for a decorator factory (a function that returns a decorator). We'll cover that in a later lesson.

Troubleshooting & edge cases

Problem: Lost function metadata

def decorator(func):
    def wrapper():
        return func()
    return wrapper

@decorator
def greet():
    """Say hello"""
    print("Hi!")

print(greet.__name__)   # 'wrapper' (not 'greet')
print(greet.__doc__)    # None (not '"Say hello"')

Fix: Use @functools.wraps — it copies __name__, __doc__, and other attributes from the original to the wrapper.

import functools

def decorator(func):
    @functools.wraps(func)
    def wrapper():
        return func()
    return wrapper

greet.__name__  # 'greet' ✓
greet.__doc__   # 'Say hello' ✓

Problem: Decorated function with varying arguments

If your wrapper doesn't accept *args, **kwargs, calling the decorated function with arguments will break.

Fix: Always use def wrapper(*args, **kwargs) in your decorator unless you're absolutely sure the decorated function takes no arguments.

Problem: Stacking multiple decorators

@timer
@logger
def fetch_data():
    pass

Order matters: this applies logger to fetch_data, then timer to the result. Execution: timer( logger(fetch_data) ). The outermost decorator runs first when the decorated function is called.

Edge case: Decorating a method in a class

The self argument is passed as the first positional argument, so the usual *args, **kwargs pattern works fine. Just be careful about instance vs. class methods.

What you learned & what's next

You now understand how to build and use decorators for functions — from writing a wrapper that adds behavior, to applying the @ syntax, to handling arguments and preserving metadata. You've seen decorators used for timing, logging, and authentication in real-world scenarios.

Key takeaways:

  • A decorator is a function that takes a function and returns a new function.
  • The @decorator syntax is equivalent to func = decorator(func).
  • Always use *args, **kwargs in the wrapper to support arbitrary function signatures.
  • Use @functools.wraps(func) to preserve original function metadata.
  • Decorators are the go-to tool for cross-cutting concerns like logging, timing, and access control.

What's next: In the next lesson, you'll learn to build decorators with arguments (e.g. @retry(max_attempts=3)) using nested factories — an essential step for building reusable, configurable decorators in production code.

Practice recap

Write a decorator called retry that calls the decorated function up to 3 times if it raises a ValueError. If all attempts fail, let the exception propagate. Test it with a function that randomly raises ValueError. This exercise reinforces the wrapper pattern and handling exceptions inside decorators — a common real-world need.

Common mistakes

  • Forgetting to return the wrapper function from the decorator — returning wrapper() calls the function immediately and breaks the decorator.
  • Using def wrapper(): without *args, **kwargs — this breaks when the decorated function accepts arguments.
  • Omitting @functools.wraps — the decorated function loses its original __name__ and __doc__, causing confusion in debugging and documentation.
  • Assuming decorators run at call time, not definition time — the @ syntax executes the decorator function when the function is defined, not when it's called.

Variations

  1. Decorators with arguments: Use a factory function that returns a decorator, e.g., @log(level='INFO').
  2. Class-based decorators: Implement __call__ in a class to maintain state across decorated function calls.
  3. Decorators on classes: @dataclass is a built-in decorator that transforms a class — decorators aren't limited to functions.

Real-world use cases

  • Logging every API call in a web framework like Flask or Django — a @log_request decorator prints method, path, and response time.
  • Measuring execution time of functions in a data pipeline to identify bottlenecks and optimize performance.
  • Checking user authentication before granting access to sensitive views in a web app — a @login_required decorator redirects unauthenticated users.

Key takeaways

  • A decorator is a function that takes a function and returns a new function, often a wrapper adding behavior before/after the original.
  • The @decorator syntax is syntactic sugar for func = decorator(func).
  • Always accept *args, **kwargs in the wrapper to handle any function signature.
  • Use @functools.wraps(func) on the wrapper to preserve the original function's name, docstring, and metadata.
  • Decorators are ideal for cross-cutting concerns like logging, timing, caching, and access control, keeping your code DRY.
  • Multiple decorators stack from bottom to top: the one closest to the function definition runs first.

Sponsored

Sponsored

Discussion

Questions, corrections, and tips help everyone reading this page.

0 comments

Add a comment

Shown publicly with your comment.

Be constructive · max 4,000 characters

No comments yet — start the thread.

Related tutorials, quizzes, and articles for this topic.