Python Decorators โ The Complete Guide
A deep, beginner-friendly walkthrough of Python decorators โ covering first-class functions, closures, the @ syntax, decorators with arguments, class decorators, functools.wraps, chaining decorators, and why decorators are one of Python's most powerful metaprogramming tools.
Last Updated
March 2026
Read Time
32 min
Level
Beginner to Intermediate
What is a Decorator in Python?
A decorator is a function that takes another function (or class) as input, wraps additional behaviour around it, and returns a new callable in its place โ all without changing a single line of the original function's own source code. In everyday terms, a decorator lets you attach reusable, extra behaviour โ logging, timing, access control, caching, validation โ to any function simply by placing a special marker directly above its definition.
That special marker is the @ syntax. Writing @my_decorator immediately above a function definition is Python's shorthand for a very ordinary idea: take the function defined below, pass it into my_decorator, and rebind the original function's name to whatever my_decorator returns. Nothing about this requires new language machinery beyond functions themselves โ decorators are, at their core, just functions that operate on other functions. The @ symbol is purely syntactic sugar that makes an extremely common pattern easier to read and write.
Decorators are only possible because Python treats functions as first-class objects. A function in Python can be assigned to a variable, stored in a list or dictionary, passed as an argument to another function, and returned as the result of another function โ exactly like an integer, a string, or any other value. Without this property, the entire decorator pattern would not exist, because a decorator's whole job is to accept a function as an ordinary argument and hand back a different function as an ordinary return value.
Decorators were added to make a specific, recurring problem easier to solve cleanly: applying the same small piece of extra behaviour to many different functions, without duplicating that behaviour's code inside every single function, and without permanently tangling unrelated concerns โ like logging or timing โ into the core logic of each function. Before decorators existed as a language feature, developers achieved the same effect manually, by writing my_function = my_wrapper(my_function) directly after each function definition. The @ syntax simply moves that same reassignment to sit visually right above the function it applies to, making the intent immediately obvious to anyone reading the code.
In modern Python, decorators are everywhere, even in code written by complete beginners. @staticmethod and @classmethod inside a class body, @property for turning a method into an attribute-like value, @app.route(...) in web frameworks like Flask, @pytest.fixture in testing frameworks, and @dataclass for automatically generating boilerplate class code are all decorators. Understanding how decorators work, therefore, is essential not just for writing your own โ it is essential for reading almost any non-trivial Python codebase.
The Problem Decorators Solve
To appreciate why decorators matter, it helps to imagine the alternative. Suppose you have ten different functions across a codebase, and you want every one of them to log how long it took to run. Without decorators, you would need to manually add timing code โ recording a start time, calling the function, recording an end time, and printing the difference โ inside each of the ten functions individually. This duplicates the same handful of lines ten times, and if you ever need to change how the timing works (say, to log to a file instead of printing), you must find and update all ten copies.
A decorator turns this scenario around completely. You write the timing logic exactly once, inside a single decorator function. Then, applying it to any function โ old or new โ is as simple as adding one line, @timer, directly above that function's definition. If the timing behaviour ever needs to change, there is exactly one place to update it, and every decorated function picks up the change automatically the next time it runs.
This distinction becomes especially valuable in three common situations. The first is cross-cutting concerns โ behaviour that logically applies across many unrelated functions, such as logging, timing, authentication checks, caching, or retry logic, none of which is really 'part of' any individual function's core purpose. The second is enforcing consistent rules across an API or framework, such as ensuring every registered web route requires a valid login, or every public function's inputs are validated the same way. The third is keeping core logic readable โ a function decorated with @cache or @require_login communicates its extra behaviour in a single word at the top, rather than burying that behaviour inside several extra lines mixed in with the function's actual work.
Decorators also solve a second, less obvious problem: they let you modify or extend behaviour without touching original source code. This matters enormously when the function being decorated lives in code you don't own or shouldn't modify โ a third-party library function, or a framework's internal method. By wrapping such a function with a decorator rather than editing it directly, you add new behaviour cleanly, from the outside, in a way that is easy to remove later and does not risk introducing bugs into code that already works.
First-Class Functions and Closures โ The Foundations Decorators Rest On
Two ideas make decorators possible in the first place: first-class functions, and closures. Understanding both โ even before writing a single decorator โ makes everything that follows far more intuitive.
In Python, a function is just another kind of object, the same way an integer or a string is. It has a type, it can be inspected, and it can be handled anywhere any other value can be handled.
def greet(): ... followed by handle = greet does not call greet โ it simply gives the function object a second name. Both names now refer to exactly the same underlying function.
A function can be passed into another function just like any other value, letting one function's behaviour be customised by handing it a different function to call internally.
A function can build and return another function as its result. This returned function can be called later, entirely independent of the function that created it.
A function can be defined entirely inside another function's body. The inner function is a completely ordinary function object โ it just happens to be created each time the outer function runs.
When an inner function references a variable from its enclosing (outer) function's scope, and is then returned or passed elsewhere, it carries that variable's value along with it โ this bundle of function plus remembered variables is called a closure.
Python resolves variable names using the LEGB rule (Local, Enclosing, Global, Built-in). A closure's remembered variables live in the Enclosing scope, one level up from the inner function's own Local scope.
If a closure needs to modify (not just read) a variable from its enclosing scope, the nonlocal keyword must be used inside the inner function to signal that assignment should update the outer variable, rather than creating a new local one.
A closure is easiest to understand through a small example: imagine an outer function make_multiplier(factor) that defines and returns an inner function multiply(x), where multiply simply computes x * factor. Even after make_multiplier has finished running and its own local scope would normally be discarded, the returned multiply function still 'remembers' the specific value of factor that was passed in when it was created. Calling make_multiplier(3) and make_multiplier(10) separately produces two independent multiply functions, each carrying its own remembered factor. This remembered state is exactly the mechanism that lets a decorator's wrapper function 'remember' the original function it is wrapping, long after the decorator itself has finished running.
Key Characteristics of Python Decorators
Decorators combine several distinct Python mechanics into one clean, reusable pattern. Here are the core characteristics every developer should internalise:
There is no special decorator type in Python. A decorator is simply any callable that accepts a function and returns a callable โ usually a new, wrapped function.
@my_decorator above a def statement is exactly equivalent to writing func = my_decorator(func) directly below the original function definition โ nothing more, nothing less.
A decorator adds behaviour around a function's call โ before it runs, after it returns, or both โ without needing to change the original function's own source code at all.
The wrapper function returned by a decorator is a closure that remembers the original function it was given, allowing it to call that original function whenever the wrapper itself is invoked.
A decorator's outer logic executes once, immediately when the decorated function is defined โ not each time the function is later called. Only the returned wrapper runs on every call.
A well-written decorator uses *args and **kwargs in its wrapper so it can decorate functions with any signature, without needing to know their specific parameters in advance.
Applying functools.wraps inside a decorator copies the original function's name, docstring, and metadata onto the wrapper, so introspection tools and documentation generators still see the original function's identity.
A decorator can itself take parameters (like @retry(times=3)) by adding an extra outer layer of function that returns the actual decorator, configured with those parameters.
Multiple decorators can be stacked above a single function, and are applied from the bottom decorator upward, with each wrapping the result of the one below it.
Decorators are not limited to functions โ a class decorator receives a class object and can add methods, wrap its __init__, or return a modified or entirely different class.
Python ships several decorators in the standard library and as built-ins, including @staticmethod, @classmethod, @property, @functools.lru_cache, and @functools.wraps itself.
Decorators let code modify how other code behaves, making them one of Python's clearest, most approachable entry points into metaprogramming.
How a Decorator Executes โ Flowchart
It helps to visualise exactly what happens, step by step, from the moment Python parses a decorated function definition to the moment the decorated function is actually called later in the program.
Code Execution Flow โ from source to output
Key insight: everything from step 2 through step 5 happens exactly once, at the moment the module is loaded and the function is defined โ it does not repeat every time say_hello() is called later. Only steps 6 through 8 repeat on every call.
How Decorators Work โ Syntax, Rebinding, and the Call Chain
To really understand decorators, it helps to separate the idea into three layers: the @ syntax itself (what Python actually does when it sees this notation), the decorator function (the code you write, which receives a function and returns another), and the wrapper function (the inner closure that actually runs in place of the original whenever the decorated name is called).
๐ค What @ Actually Means
Writing @my_decorator directly above def say_hello(): is parsed by Python as pure syntactic sugar for a two-step process that could equally be written out by hand: first define say_hello normally, and then immediately reassign say_hello = my_decorator(say_hello). The @ syntax exists purely so this reassignment can sit visually right above the function it modifies, rather than being written out separately below โ a small but meaningful readability improvement once you're applying several decorators across a file.
๐๏ธ The Decorator Function's Job
A decorator function receives exactly one argument โ the function being decorated โ and must return a callable to take its place. In the overwhelming majority of real decorators, that returned callable is a small inner function, conventionally named wrapper, defined right inside the decorator's own body. Because this inner function is defined inside the decorator, it forms a closure over the original function, meaning it can call that original function later, long after the decorator itself has already finished running and returned.
๐ฏ What Happens When the Decorated Function Is Called
After decoration, the name say_hello no longer refers to the original function at all โ it refers to the wrapper function that the decorator returned. So when your code later calls say_hello(), it is actually calling wrapper(). Inside wrapper, the decorator's author is free to run any code before calling the original function, any code after it returns, to change the arguments passed to it, to change the value it returns, or even to skip calling the original function altogether under some condition.
๐ฅ Handling Arbitrary Function Signatures
A decorator meant to be reusable across many different functions cannot know in advance what parameters those functions will accept. The standard solution is to give the wrapper function the signature def wrapper(*args, **kwargs):, which accepts any combination of positional and keyword arguments, and then forward them unchanged into the call to the original function inside the wrapper โ original_function(*args, **kwargs). This pattern is so common that recognising *args, **kwargs inside a wrapper is often the fastest way to identify a general-purpose decorator at a glance.
๐ Why the Original Function Object Never Disappears
A question that trips up many newcomers is: once the name say_hello has been reassigned to point at wrapper, what happened to the original function object itself? The answer is that it hasn't gone anywhere โ it still exists, fully intact, in memory. It simply no longer has a name bound to it at the module level; instead, the only remaining reference to it lives inside the closure of the wrapper function that the decorator created. This is exactly why the wrapper is able to call it later: the wrapper's closure holds a direct reference to the original function object, keeping it alive for as long as the wrapper itself exists, even though its original module-level name has been reused for something else entirely.
๐งพ Positional-or-Keyword Ambiguity and Argument Ordering
One easy-to-overlook subtlety when writing a general-purpose wrapper is that Python does not by itself know which of the original function's parameters were meant to be positional and which were meant to be keyword-only โ it simply forwards whatever combination of args and kwargs it happened to receive from the caller. This is usually harmless, since forwarding *args, **kwargs preserves whatever calling convention the original caller used. It becomes relevant only when a decorator's own logic needs to inspect or modify a specific argument by name โ for example, a validation decorator that wants to check the value of a parameter called user_id regardless of whether the caller passed it positionally or as a keyword. In such cases, decorator authors often reach for inspect.signature() to bind the incoming args and kwargs against the original function's actual parameter names, producing a dictionary-like mapping that can be inspected reliably no matter how the caller chose to supply their arguments.
๐งญ Decorators and Return Values
It's worth being explicit about a detail that is easy to get wrong when first writing a wrapper: whatever the wrapper function itself returns is what the caller of the decorated function actually receives โ not automatically whatever the original function returned. If a wrapper calls func(*args, **kwargs), captures the result in a variable, runs some additional logic, but then forgets to include a final return result statement, the decorated function will silently start returning None to every caller, even though the original undecorated function always returned a meaningful value. This is one of the more common, and more confusing, decorator bugs precisely because the function still appears to 'work' โ it runs without raising any error โ while quietly discarding its actual return value.
functools.wraps โ Preserving Function Identity
There is a subtle but important side effect of decorating a function the naive way: the name that used to point to the original function now points to the wrapper function instead. This means that unless something is done about it, introspection attributes like __name__, __doc__, and __module__ on the decorated function will actually report the wrapper's own metadata โ typically the literal string 'wrapper' as the function's name, and None as its docstring โ rather than anything describing the original function.
This matters more than it might first appear. Debugging tools, error tracebacks, automatically generated documentation, and testing frameworks all frequently rely on a function's __name__ and __doc__ to describe what went wrong or what a piece of code does. If every decorated function in a codebase reports itself simply as 'wrapper', tracing bugs and generating accurate documentation both become noticeably harder.
The standard library solves this with functools.wraps, itself a decorator, applied to the inner wrapper function inside your own decorator's definition. Applying @functools.wraps(original_function) directly above the def wrapper(...) line copies the original function's __name__, __doc__, __module__, and a handful of other attributes onto the wrapper, so that the decorated function continues to look, to every introspection tool, exactly like the function it actually is. As a rule, any decorator intended for reuse beyond a single quick script should always apply functools.wraps to its wrapper โ omitting it is one of the most common small mistakes in decorator code.
Decorators vs Manual Wrapping vs Inheritance โ Key Differences
Beginners sometimes wonder whether a decorator is really necessary, when the same effect could seemingly be achieved by manually wrapping a function call, or by using inheritance in a class-based design. This comparison lays out how each approach actually behaves.
Decorator Internal Architecture Diagram
The diagram below shows what actually happens, layer by layer, from the moment Python parses a decorated function definition down to the eventual call of the original function inside the wrapper.
Your First Python Decorator
Just as every Python journey starts with a Hello World program, every decorator journey starts with a tiny decorator that simply announces when a function starts and finishes. It beautifully illustrates the core idea: extra behaviour wraps around the original call.
import functools
def announce(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print(f"Starting {func.__name__}")
result = func(*args, **kwargs)
print(f"Finished {func.__name__}")
return result
return wrapper
@announce
def say_hello(name):
print(f"Hello, {name}!")
say_hello("Ashish")Output
Starting say_hello Hello, Ashish! Finished say_helloPractice This Code โ Live Editor
Line-by-Line Explanation
- โถ
def announce(func):โ the decorator itself; it receives whatever function will be decorated as its single parameter, here named func. - โถ
@functools.wraps(func)โ applied to the inner wrapper, this copies func's __name__ and __doc__ onto wrapper so the decorated function's identity is preserved. - โถ
def wrapper(*args, **kwargs):โ the inner closure that will actually run in place of the original function; *args/**kwargs let it accept any signature. - โถ
result = func(*args, **kwargs)โ calls the original function, forwarding whatever arguments the caller supplied, and captures its return value. - โถ
return wrapperโ the decorator hands back the wrapper function; this is what gets bound to say_hello after decoration. - โถ
@announceabove def say_hello(name): โ equivalent to writing say_hello = announce(say_hello) directly after the function's definition.
Decorators That Accept Their Own Arguments
So far, every decorator shown takes exactly one argument โ the function being decorated โ because that is what the @decorator syntax passes to it directly. But it is extremely common to want a decorator that is itself configurable, written as @retry(times=3) or @cache(expire_seconds=60), where the value inside the parentheses customises the decorator's behaviour. Achieving this requires one additional layer of nested functions.
The trick is that @retry(times=3) is not, by itself, a decorator โ it is a call that must first return a decorator, which Python then applies to the function underneath it. So a decorator-with-arguments is really structured as three nested layers: an outer function that accepts the configuration arguments (times, in this example) and returns the actual decorator; a middle function โ the real decorator โ that accepts the function to be decorated and returns the wrapper; and the innermost wrapper function itself, which runs on every call and has access, via closures, to both the configuration arguments and the original function.
- โถ
def retry(times):โ the outermost layer, called directly as retry(3); its job is only to remember times and return the real decorator. - โถ
def decorator(func):โ the middle layer, which Python calls automatically with the function underneath @retry(3); its job is to remember func and return the wrapper. - โถ
def wrapper(*args, **kwargs):โ the innermost layer, which actually runs on every call, using both times and func from the enclosing closures to implement the retry logic. - โถ
@retry(times=3)abovedef fetch_data():โ expands to fetch_data = retry(times=3)(fetch_data), clearly showing the two separate calls happening: first configuring the decorator, then applying it.
A closely related, more flexible pattern lets the same decorator be used both with and without parentheses โ for example allowing both @my_decorator and @my_decorator(option=True) to work on the same decorator implementation. This is typically achieved by checking, inside the outer function, whether it was called directly with a function (the no-parentheses case) or with keyword configuration (the with-parentheses case), and branching accordingly. While convenient for library authors, this dual-mode pattern adds real complexity, and most decorators are perfectly fine committing to just one style โ either always used with parentheses, or never.
Class Decorators and Decorating Methods
Everything covered so far has decorated plain functions, but the decorator pattern applies equally to methods inside a class and to entire classes themselves, since both are, underneath, still just callables that a decorator can accept and replace.
๐ง Decorating Methods
A decorator applied to a method inside a class body works essentially the same way as one applied to a standalone function, with one detail to keep in mind: an instance method's first parameter is conventionally self, so a wrapper intended to decorate methods must forward self along with everything else โ which the general *args, **kwargs pattern already handles correctly, since self simply arrives as the first positional argument like any other.
๐๏ธ Class Decorators
A class decorator is a decorator applied directly above a class definition rather than a def. Python passes the entire class object into the decorator, and whatever the decorator returns takes the class's place, exactly as with functions. A class decorator can add new methods or attributes onto the class, wrap existing methods (including __init__), record every class it decorates into some external registry, or even return a completely different class or a factory function in its place. The built-in @dataclasses.dataclass decorator is a well-known real-world example: applying it to a class automatically generates an __init__, a readable __repr__, and comparison methods, purely by inspecting the class's declared type-annotated attributes.
๐งฌ Decorators vs Metaclasses
Class decorators are often compared to metaclasses, since both can modify a class's final shape. The key difference is scope and simplicity: a class decorator runs once, after the class body has already been fully executed and the class object already exists, and simply returns a (possibly modified) class in its place. A metaclass, by contrast, controls the entire process of how the class is constructed in the first place. For the overwhelming majority of practical needs โ adding methods, registering classes, validating class-level configuration โ a class decorator is simpler to write, easier to reason about, and should be reached for well before a metaclass is considered.
Chaining (Stacking) Multiple Decorators
It is entirely valid, and common in real code, to apply more than one decorator to the same function by stacking several @decorator lines directly above its definition. Understanding the exact order in which stacked decorators are applied is essential, because it directly determines the order in which their wrapped behaviour actually executes.
When several decorators are stacked, Python applies them from the bottom upward โ the decorator closest to the function's own def line runs first, wrapping the original function, and each decorator above it then wraps the result of the one below. So with @a, @b, and @c stacked in that order above def func():, the actual effect is equivalent to func = a(b(c(func))). This means that, at call time, execution order is reversed relative to how the wrapping happened: the outermost decorator's pre-call logic runs first (since it's the outermost wrapper being called), then the next one in, and so on inward, until finally the original function itself runs at the very centre โ after which each wrapper's post-call logic unwinds back outward in the opposite order.
- โถ
Stacking order in source code, top to bottom:
@a, then@b, then@c, thendef func(): - โถ
Equivalent explicit wrapping:
func = a(b(c(func)))โ c wraps the original function first, then b wraps that, then a wraps that - โถ
Execution order when func() is finally called: a's pre-call logic โ b's pre-call logic โ c's pre-call logic โ original function body โ c's post-call logic โ b's post-call logic โ a's post-call logic
- โถ
A useful mental model: reading decorators from the function definition upward tells you the order in which their logic 'wraps inward' toward the original call
This ordering has real practical consequences. A decorator that checks user authentication should typically be stacked above (outside) a decorator that does expensive logging or caching, so that unauthenticated calls are rejected before any of that extra work is even attempted. Similarly, a decorator that catches and logs exceptions generally needs to sit outside any decorator whose own code might itself raise an exception, so that the logging decorator actually gets a chance to see and record it.
Common Built-In and Standard Library Decorators
Python itself, and its standard library, ship a handful of decorators that every developer encounters early on. Recognising what each one actually does under the hood demystifies a lot of otherwise-unexplained class syntax.
- โถ
@staticmethodโ marks a method that does not receive self (or cls) at all, behaving like a plain function that simply happens to live inside a class's namespace, mainly for organisational purposes. - โถ
@classmethodโ marks a method that receives the class itself (conventionally named cls) as its first argument instead of an instance, commonly used for alternative constructors. - โถ
@propertyโ turns a method into something that can be accessed like a plain attribute (without parentheses), commonly used to expose computed or validated values while keeping a clean, attribute-style interface. - โถ
@functools.wrapsโ copies metadata (name, docstring, module) from an original function onto a wrapper, preserving the decorated function's identity for introspection. - โถ
@functools.lru_cache(maxsize=...)โ automatically caches a function's return values based on its arguments, so repeated calls with the same arguments skip recomputation entirely. - โถ
@functools.singledispatchโ lets a function behave differently depending on the type of its first argument, effectively enabling simple type-based overloading in a dynamically typed language. - โถ
@dataclasses.dataclassโ automatically generates boilerplate methods like __init__, __repr__, and __eq__ for a class, based on its declared, type-annotated attributes. - โถ
@contextlib.contextmanagerโ turns a specially structured generator function into a context manager usable with Python's with statement, without needing to write a full __enter__/__exit__ class.
Studying the source code of a few of these โ especially functools.lru_cache and contextlib.contextmanager โ is one of the best ways to deepen your understanding of decorators, since they demonstrate real, production-quality decorator patterns solving genuinely useful problems, well beyond simple logging or timing examples.
Advantages and Disadvantages of Decorators
Decorators are a powerful tool, but like every feature in Python, they come with genuine trade-offs. Knowing both sides helps you decide when reaching for a decorator is the right call โ and when simpler, more explicit code is actually the better choice.
Debugging and Testing Decorated Functions
Decorated functions introduce an extra layer of indirection that is worth understanding specifically from a debugging and testing perspective, since it changes how you should approach both activities compared to plain, undecorated functions.
When stepping through decorated code with an interactive debugger, you will typically find yourself stepping into the wrapper function first, rather than landing directly inside the original function's body. This is expected and correct behaviour โ remember that the decorated name genuinely does point to the wrapper, so calling it genuinely does begin execution there. Once inside the wrapper, stepping further (rather than stepping over) will eventually take you into the original function's own code at the point where the wrapper calls it. Being comfortable with this extra hop is simply part of working effectively with decorated code, and most debuggers offer a way to step over uninteresting wrapper frames once you're used to the pattern.
Testing decorated functions deserves a small amount of extra care as well. If a decorator adds behaviour that is expensive, has side effects (like writing to a log file or a cache), or depends on external state (like a live network connection for a retry decorator), unit tests for the function's own core logic can become entangled with testing the decorator itself. A common, effective approach is to test the undecorated function's core logic separately โ often by testing the plain function before decoration is applied, or by exposing it via a __wrapped__ attribute, which functools.wraps conveniently sets automatically to point back at the original, undecorated function โ and then write separate, focused tests specifically for the decorator's own added behaviour, using a small, deliberately simple function purely as a test subject.
Where Are Decorators Used? โ Real-World Applications
Decorators are not just a classroom curiosity โ they underpin real, everyday Python code across many domains. Here are the major places you will encounter, or should reach for, decorators in 2026:
- โถ
๐ Web Framework Routing โ Frameworks like Flask and FastAPI use decorators such as @app.route("/users") or @app.get("/items") to associate a URL path directly with the function that should handle requests to it, keeping routing declarations right next to the handler logic they describe.
- โถ
๐ Authentication and Authorisation โ Decorators like @login_required or @requires_permission("admin") are a common way to enforce access control consistently across many view functions or API endpoints, without repeating the same check inside every one.
- โถ
๐ Logging and Monitoring โ Wrapping functions with a logging decorator records when they were called, with what arguments, and what they returned (or whether they raised an exception), providing consistent observability across an entire codebase.
- โถ
โฑ๏ธ Timing and Performance Profiling โ A simple timing decorator records how long a function takes to execute, which is invaluable for quickly identifying slow code paths during development and performance tuning.
- โถ
๐พ Caching / Memoisation โ Decorators like functools.lru_cache automatically store the results of expensive function calls, returning cached results instantly for repeated calls with the same arguments instead of recomputing them.
- โถ
๐งช Testing Frameworks โ Testing libraries like pytest use decorators extensively โ @pytest.fixture to set up reusable test data, and @pytest.mark.parametrize to run the same test function against many different input values.
- โถ
๐ Retry Logic โ A @retry(times=3) style decorator automatically re-attempts a function call that fails due to a transient error (such as a flaky network request), without cluttering the function's own logic with retry loops.
- โถ
โ Input Validation โ Decorators can inspect a function's arguments before it runs, rejecting invalid input early with a clear error, keeping validation logic separate from the function's main computation.
Common Mistakes When Working With Decorators
Even experienced developers occasionally trip over a handful of recurring decorator pitfalls. Being aware of them ahead of time saves a lot of confused debugging later.
- โถ
Forgetting functools.wraps โ Omitting @functools.wraps(func) on the inner wrapper silently replaces the decorated function's __name__ and __doc__ with the wrapper's own, breaking introspection, debugging output, and auto-generated documentation.
- โถ
Not Forwarding All Arguments โ Writing a wrapper with a fixed signature instead of *args, **kwargs makes the decorator break the moment it's applied to a function with a different set of parameters than the one it was originally written and tested against.
- โถ
Confusing @decorator with @decorator() โ Applying a decorator-with-arguments without its parentheses (or vice versa for a plain decorator) is a very common source of confusing TypeErrors, since Python is either calling the wrong thing, or not calling something it needed to.
- โถ
Assuming Decorator Code Runs on Every Call โ Code written directly inside the outer decorator function (outside the inner wrapper) runs only once, at decoration time โ not on every call to the decorated function. Logic meant to run per-call must live inside the wrapper itself.
- โถ
Misjudging Stacking Order โ Assuming stacked decorators are applied top-down rather than bottom-up leads to authentication, logging, or caching decorators running in an unintended order relative to each other.
- โถ
Overusing Decorators for Simple Logic โ Wrapping a function in a decorator purely to run one or two lines of straightforward code that would be just as clear written directly inside the function itself adds indirection without meaningful benefit.
Why Should You Learn Decorators in 2026?
Decorators are frequently described as an 'intermediate' Python topic, but in 2026 they are functionally a core skill, not an optional extra. Here's why every Python developer should be comfortable with them:
- โถ
๐ Every Modern Framework Relies on Them โ Flask, FastAPI, Django, pytest, and countless other widely used frameworks and libraries expose their core functionality through decorators; being unable to read decorator syntax comfortably makes working with any of them noticeably harder.
- โถ
๐งต A Gateway to Deeper Python Understanding โ Learning decorators forces a genuine, practical understanding of first-class functions and closures โ concepts that pay off well beyond decorators themselves, throughout functional-style Python code.
- โถ
๐ผ A Common Interview Topic โ Questions about how the @ syntax works, why functools.wraps matters, or how decorator stacking order affects execution are among the most frequently asked intermediate-level Python interview questions.
- โถ
๐งน Cleaner, More Maintainable Code โ Once comfortable with decorators, a surprising number of everyday cross-cutting concerns โ logging, timing, caching, access control โ can be factored out of core logic and reused consistently across an entire codebase.
- โถ
๐ช A Foundation for Metaprogramming โ Decorators are one of the most approachable entry points into Python's broader metaprogramming capabilities, making later, more advanced topics like descriptors and metaclasses easier to approach.
The Evolution of Decorators Across Python Versions
Decorators have grown steadily since their introduction, gaining new syntax and standard library support that expanded them from a niche pattern into a foundational language feature.
- โถ
Python 2.2 โ staticmethod and classmethod were introduced as built-in functions, though initially they had to be applied with an explicit reassignment rather than dedicated syntax.
- โถ
Python 2.4 โ The dedicated @ decorator syntax (PEP 318) was introduced for functions, making the reassignment pattern that developers were already using by hand into a first-class, readable part of the language.
- โถ
Python 2.6 / 3.0 โ The functools module gained wraps() as the standard, recommended way to preserve a decorated function's metadata, quickly becoming best practice for any serious decorator.
- โถ
Python 2.6 โ Class decorators (PEP 3129/related work) became broadly supported, extending the @ syntax to apply directly above class definitions as well as function definitions.
- โถ
Python 3.2 โ functools.lru_cache was added to the standard library, offering a ready-made, highly optimised memoisation decorator without needing to write one from scratch.
- โถ
Python 3.4 โ functools.singledispatch was introduced, bringing simple type-based function overloading to Python via a decorator-based API.
- โถ
Python 3.7 โ The dataclasses module and its @dataclass decorator were introduced, automatically generating common boilerplate methods for simple data-holding classes.
- โถ
2024-2026 โ Modern Python (3.12/3.13+) relaxed several historical syntactic restrictions on what expressions can appear after @, making certain advanced decorator-composition patterns easier to write, while decorator usage across frameworks and the standard library continues to grow every release.
Decorator Interview Questions โ Beginner to Intermediate
These are among the most frequently asked Python interview questions about decorators, at both fresher and intermediate levels.
Practice Questions โ Test Your Knowledge
Test your understanding of decorators with these practice questions. Try answering each one yourself before checking the explanation โ active recall is the most effective way to learn.
1. If a decorator's outer function contains a print() statement outside the inner wrapper, when does that print statement actually run?
Easy2. What is the output, in terms of order, of a decorator that prints 'before' then calls the function then prints 'after', applied to a function that itself prints 'inside'?
Easy3. Why does a decorated function's help() output sometimes show the wrong docstring, and how do you fix it?
Medium4. Given @a, @b stacked above a function func(), which decorator's wrapper actually calls func() directly?
Medium5. Why would a decorator with a fixed wrapper(x, y) signature fail when applied to a function that takes three arguments?
Medium6. How would you write a decorator, @repeat(times), that calls the decorated function times number of times in a row?
Hard7. Why can nonlocal be necessary inside a decorator's wrapper function, for example when building a call-counting decorator?
Hard8. What would go wrong if a caching decorator used a mutable default argument, like an empty dictionary, as its cache storage defined directly in the decorator's own parameter list rather than inside its closure?
HardConclusion โ Should You Use Decorators?
Decorators are one of Python's most elegant ideas: a way to attach reusable, extra behaviour to any function or class, without editing that function's own source code, using nothing more exotic than ordinary functions, closures, and a small piece of syntactic sugar. Instead of forcing you to duplicate the same cross-cutting logic across many functions, or bury it inline alongside each function's core purpose, decorators let you write that logic exactly once and apply it anywhere with a single, readable line.
If you find yourself repeating the same handful of lines โ timing, logging, validation, access checks โ across several unrelated functions, a decorator is very likely the right tool to reach for. If you are working with a web framework, testing library, or any tool that exposes its API through @-syntax, understanding decorators is what turns that syntax from unexplained magic into a pattern you can read, predict, and eventually extend yourself. And if you eventually explore Python's broader metaprogramming features โ descriptors, metaclasses, and beyond โ a solid grasp of decorators is the natural, approachable first step.
The next step in mastering decorators is practising with real scenarios โ write a small timing decorator and apply it to a few of your own functions, build a decorator that accepts configuration arguments like @retry(times=3), and experiment with stacking two or three decorators together to see the bottom-up application order in action. The more deliberately you use decorators, the more naturally you will start to recognise decorator-shaped problems across your own codebase.
Decorators are not an obscure advanced trick โ they are core, idiomatic Python. Once first-class functions, closures, and the @ syntax click together, you will start reading framework code, testing libraries, and standard-library modules with genuinely new understanding, and your own code will become cleaner and more reusable as a direct result. ๐