Python Intermediate

Python Decorators: From Basics to Advanced

Understand Python decorators from scratch. Learn to write your own decorators for logging, authentication, caching, and more.

DjangoZen Team Mar 29, 2026 16 min read 172 views

Decorators are one of Python's most elegant features and one that intimidates many developers, because the syntax looks like magic and the underlying concept — functions that take functions and return functions — bends the mind at first. But decorators are everywhere in Python, from web frameworks to standard-library tools, and understanding them transforms code you previously copied without comprehending into something you can read and write fluently. This tutorial builds decorators from first principles to advanced patterns.

Functions are objects

The foundation that makes decorators possible is that in Python, functions are first-class objects: you can assign a function to a variable, pass it as an argument to another function, and return it from a function, just like any other value. This is not true in every language, and it is the single insight everything about decorators depends on. Once you accept that a function is just an object you can pass around and manipulate, the idea of a function that takes another function and gives back a modified version stops being strange. Internalizing that functions are values you can hand around is the prerequisite for everything that follows.

What a decorator is

A decorator is a function that takes another function and returns a new function, usually one that wraps the original to add behavior before or after it runs. The decorator does not change the original function's code; it wraps it in additional logic. This wrapping pattern is what lets you add functionality — logging, timing, access checks — to a function without modifying the function itself. Understanding a decorator as a wrapper-maker, a function whose job is to take your function and return an enhanced version of it, is the core concept, and everything else is syntax and refinement around this idea.

The @ syntax

Python provides the @ symbol as syntactic sugar for applying a decorator. Writing the decorator name with an @ on the line above a function definition tells Python to pass that function through the decorator and replace it with the result:

@my_decorator
def greet():
    print("Hello")

This is exactly equivalent to defining the function and then reassigning it to the decorated version, just more readable. The @ syntax is purely convenience — it does nothing you could not do manually — but it places the decorator visibly above the function, making the enhancement clear. Recognizing that the symbol is shorthand for wrapping the function demystifies what looks like special magic.

A simple decorator

The classic first decorator wraps a function to run code before and after it. Inside the decorator you define a wrapper function that does something, calls the original function, does something else, and returns the result, then the decorator returns that wrapper:

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

Applying this with @logged makes the function print messages around its execution without changing its own code. This pattern — define a wrapper, call the original inside it, return the wrapper — is the template nearly every decorator follows.

Handling arguments with *args and **kwargs

A decorator should work on functions regardless of what arguments they take, which is why the wrapper accepts *args and **kwargs — capturing any positional and keyword arguments — and passes them through to the original function. This makes the decorator general: it does not need to know the signature of the function it wraps, it simply forwards whatever arguments it receives. Understanding this is what lets one decorator apply to many different functions with different signatures. The *args, **kwargs pattern in the wrapper is what gives a decorator its reusability across all the varied functions you might want to enhance.

Preserving metadata with functools.wraps

A subtle problem with the basic pattern is that the wrapper replaces the original function, so the decorated function loses its original name, docstring, and other metadata — it now reports itself as the wrapper. The fix is the functools.wraps decorator, applied to your wrapper, which copies the original function's metadata onto the wrapper so the decorated function still looks like itself. This is a small but important detail for writing well-behaved decorators, because losing the function's identity breaks introspection and documentation tools. Using functools.wraps in every decorator you write is a best practice that keeps decorated functions transparent about what they really are.

Decorators that take arguments

Sometimes you want a decorator you can configure — for example, one that retries a function a specified number of times. This requires an extra layer: a function that takes the configuration arguments and returns a decorator, which in turn takes the function and returns the wrapper. The three nested levels look daunting but follow logically from the two-level pattern, with one more layer added to capture the arguments. Understanding that a parameterized decorator is just a function that builds and returns a decorator based on its arguments is what lets you read and write the configurable decorators that appear throughout real codebases and frameworks.

Stacking multiple decorators

You can apply more than one decorator to a function by stacking them, each on its own line above the function. They apply from the bottom up — the one closest to the function wraps it first, then the next wraps that, and so on — so the order matters. Stacking lets you compose behaviors, combining a timing decorator with a logging decorator with an access-check decorator on a single function. Understanding the bottom-up application order is important because it determines the sequence in which the wrappers run, and getting it right is necessary when the decorators interact, such as when one must run before another for the combination to behave correctly.

Decorating with and as classes

Decorators are not limited to functions. You can write a decorator as a class, where the class wraps the function and its call method runs the wrapping logic, which is useful when the decorator needs to maintain state. You can also decorate classes themselves, modifying a class definition. These are more advanced uses, but they follow the same principle of taking something and returning an enhanced version. Knowing that the decorator pattern extends to classes — both writing decorators as classes and applying decorators to classes — rounds out your understanding and prepares you for the full range of decorator usage you will encounter in sophisticated Python code.

Decorators in the wild

Decorators are pervasive in real Python, which is the practical reason to understand them. Web frameworks use them to mark routes, require login, or cache responses; the standard library provides decorators for caching results, defining properties, and registering methods; testing tools use them to parameterize and skip tests. Once you understand decorators, all of this becomes readable rather than mysterious, and you can write your own to eliminate repetition in your code. Seeing how widely the pattern is used reinforces why it is worth mastering: decorators are not an obscure trick but a fundamental Python idiom you will meet constantly.

When to use them

Decorators shine for cross-cutting concerns — behavior you want to add to many functions without repeating it, like logging, timing, caching, access control, or retrying. They keep that behavior in one place and apply it cleanly with a single line. The caution is not to overuse them or hide too much logic in a decorator, where it becomes invisible action at a distance that obscures what a function really does. Used judiciously for genuine cross-cutting concerns, decorators make code cleaner and more expressive; used excessively, they can make it harder to follow. Knowing when they add clarity rather than hide it is the final piece of using them well.

Closures: the idea underneath

Decorators rely on a concept worth naming: closures. When the inner wrapper function refers to the original function passed into the decorator, it "closes over" that function, retaining access to it even after the decorator has finished running. This is what lets the wrapper call the original later. Closures — inner functions that remember the variables from the scope they were created in — are the mechanism that makes the decorator pattern work. Understanding closures clarifies why the wrapper can still reach the original function it wraps, and it is a powerful Python concept in its own right that appears beyond decorators wherever a function needs to carry some context with it.

The most common decorator uses

A few uses dominate in practice. Timing decorators measure how long a function takes, useful for performance work. Logging decorators record when functions are called and with what, aiding debugging. Caching decorators store a function's results so repeated calls with the same arguments return the saved result instead of recomputing. Access-control decorators check permissions before allowing a function to run. Retry decorators re-attempt a function that fails. Recognizing these common patterns helps you both understand decorators you encounter and identify where writing your own would clean up repetition in your code, since these recurring needs are exactly what decorators handle elegantly.

Debugging decorated functions

One practical wrinkle is that decorators add a layer between you and the original function, which can complicate debugging — a traceback may show the wrapper, and the function's identity can be obscured if you did not preserve its metadata. This is precisely why using the metadata-preserving helper matters, and why keeping decorators simple and well-tested is wise. When debugging code that uses decorators, remembering that there is a wrapping layer helps you interpret what you see. Understanding that decorators introduce indirection — and handling it by preserving metadata and keeping wrappers clear — keeps the convenience of decorators from turning into confusion when something goes wrong inside a decorated function.

Writing good decorators

A few principles make for good decorators. Keep them focused on one concern, so a decorator does a single clear thing. Always preserve the wrapped function's metadata. Make sure the wrapper passes through arguments and return values faithfully, so the decorated function behaves like the original plus the added behavior. And do not hide too much surprising logic, since a decorator that drastically changes behavior invisibly is hard to reason about. Following these principles produces decorators that are reusable, transparent, and pleasant to work with. Understanding what separates a good decorator from a confusing one helps you write enhancements that genuinely clean up your code rather than adding hidden complexity.

Summary

Decorators rest on a single idea: in Python, functions are objects you can pass around and return, so a decorator is simply a function that takes a function and returns an enhanced, wrapping version of it. The @ syntax is convenient shorthand for this wrapping. The standard pattern defines a wrapper that accepts *args, **kwargs so it works on any function, runs logic before and after calling the original, and returns the result — and applying functools.wraps keeps the decorated function's identity intact. From there, decorators that take arguments add one nesting layer, stacking composes multiple decorators bottom-up, and the pattern extends to classes. Decorators are everywhere in real Python — frameworks, the standard library, testing tools — which is why understanding them turns mysterious code readable. Use them for cross-cutting concerns to eliminate repetition cleanly, and with this foundation, what once looked like magic becomes a tool you wield with confidence.