What Is A Decorator In Python

8 min read

In Python, a decorator is a function that adds extra behavior to another function or class without permanently modifying the original function’s code. Decorators are a powerful feature used for logging, authorization, timing, caching, validation, retries, and many other reusable tasks. They are one of the features that makes Python flexible, readable, and expressive.

What Is a Decorator in Python?

A decorator is essentially a higher-order function, meaning it is a function that takes another function as an argument and returns a new function, usually with additional behavior added around the original function.

The most common use of decorators is to wrap a function so that something happens before or after the original function runs That's the whole idea..

Take this: suppose you have a simple function:

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

You could decorate it like this:

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

The line:

@decorator_name

is equivalent to writing:

say_hello = decorator_name(say_hello)

This means the decorator receives the original function and replaces it with another function.

Why Use Decorators?

Decorators are useful because they help you follow the DRY principle, which means “Don’t Repeat Yourself.” Without decorators, you might end up writing the same code again and again Small thing, real impact..

To give you an idea, imagine you want to measure how long different functions take to run. You could manually add timing code to every function:

import time

start = time.time()
add(2, 3)
end = time.time()

print(end - start)

But if you have many functions, this becomes repetitive and messy. A decorator lets you apply the same timing logic to multiple functions with one reusable piece of code That's the whole idea..

Decorators are commonly used for:

  • Logging function calls
  • Checking user permissions
  • Measuring execution time
  • Retrying failed operations
  • Caching function results
  • Validating input arguments
  • Formatting output
  • Adding authentication to web routes

Basic Decorator Example

Let’s create a simple decorator that prints a message before and after a function runs That's the part that actually makes a difference..

def my_decorator(func):
    def wrapper():
        print("Something is happening before the function is called.")
        func()
        print("Something is happening after the function is called.")
    return wrapper

Now we can use it:

@my_decorator
def say_welcome():
    print("Welcome!")

When you call:

say_welcome()

The output will be:

Something is happening before the function is called.
Welcome!
Something is happening after the function is called.

The decorator works by creating a wrapper function inside the decorator. This wrapper function calls the original function and adds extra behavior around it.

How the Decorator Works Step by Step

Let’s break down what happens in this example:

def my_decorator(func):
    def wrapper():
        print("Before")
        func()
        print("After")
    return wrapper
  1. my_decorator receives the original function as an argument.
  2. It defines a new function called wrapper.
  3. The wrapper function calls the original function.
  4. The decorator returns the wrapper function.
  5. The original function name now refers to the wrapper function.

So when you write:

@my_decorator
def say_welcome():
    print("Welcome!")

Python rewrites it internally as:

say_welcome = my_decorator(say_welcome)

It's why decorators are sometimes described as “functions that modify or enhance other functions.”

Preserving Function Metadata with functools.wraps

A common problem with decorators is that they replace the original function with the wrapper function. This can affect function metadata such as the function name, docstring, and annotations.

Take this: without functools.wraps, the decorated function may appear to have the name wrapper instead of its original name.

To avoid this, Python provides the functools.wraps decorator from the standard library.

from functools import wraps

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

The @wraps(func) line copies important metadata from the original function to the wrapper function Surprisingly effective..

What this tells us is if you inspect the decorated function, you may see its original name and documentation.

For example:

@my_decorator
def greet():
    """Greets the user."""
    print("Hello!")

Now greet.__name__ will correctly return "greet" instead of "wrapper".

Using functools.wraps is considered best practice when writing decorators.

Decorators with Arguments

Many real-world decorators need to accept arguments. Here's one way to look at it: a logging decorator might accept a message, or a retry decorator might accept the number of attempts Easy to understand, harder to ignore..

To support arguments, decorators need one extra level of nesting.

def repeat(times):
    def decorator(func):
        def wrapper(*args, **kwargs):
            for _ in range(times):
                result = func(*args, **kwargs)
            return result
        return wrapper
    return decorator

You can use it like this:

@repeat(times=3)
def greet(name):
    print(f"Hello, {name}!")

Calling the decorated function:

greet("Alice")

will print:

Hello, Alice!
Hello, Alice!
Hello, Alice!

This structure is important:

def repeat(times):
    def decorator(func):
        def wrapper(*args, **kwargs):
            ...
        return wrapper
    return decorator

The outer function receives the decorator arguments. The middle function receives the original function. The inner wrapper receives the function arguments when the decorated function is called.

Passing Arguments to Decorated Functions

A good decorator should support regular function arguments. This is why many wrappers use *args and **kwargs.

from functools import wraps

def my_decorator(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print("Before the function is called.")
        result = func(*args, **kwargs)
        print("After the function is called.")
        return result
    return wrapper

Now the decorated function can accept any arguments:

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

result = add(2, 3)
print(result)

Output:

Before the function is called.
After the function is called.
5

The wrapper returns the original function’s result, which is important because many decorated functions are expected to produce a value And that's really what it comes down to. Nothing fancy..

Function Decorators vs. Class Decorators

Most decorators work with functions, but Python also allows decorators to modify classes.

A class decorator receives a class as input and returns a modified or enhanced class.

For example:

def add_metadata(cls):
    cls.company = "Python Academy

### Completing the Class Decorator Example

The snippet that began the class‑decorator section left the modification of the class unfinished. A typical implementation would assign additional attributes or even replace the class definition itself. Here’s a concise continuation that demonstrates both possibilities:

```python
def add_metadata(cls):
    # Attach a new class‑level attribute
    cls.company = "Python Academy"

    # Optionally inject a custom method that uses the new attribute
    def get_company(self):
        return cls.company

    # Bind the new method to the class so that it receives the instance
    cls.get_company = get_company

    return cls

When the decorator is applied:

@add_metadata
class Service:
    def __init__(self, name):
        self.name = name

the resulting Service class now carries a company attribute and a get_company method that can be called on instances:

svc = Service("Demo")
print(svc.get_company())   # → Python Academy
print(Service.company)     # → Python Academy

This pattern is useful for frameworks that need to register classes, attach configuration data, or enrich the class with helper utilities Not complicated — just consistent. Surprisingly effective..

Method Decorators: Preserving self and cls

When decorating methods, the wrapper must retain the descriptor protocol so that the bound self (for instance methods) or cls (for class methods) is passed correctly. The most reliable way to achieve this is by using functools.wraps together with the appropriate update_wrapper call:

def log_call(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        # `args[0]` is the instance for instance methods,
        # `args[0]` is the class for class methods.
        print(f"Calling {func.__name__}")
        result = func(*args, **kwargs)
        print(f"{func.__name__} finished")
        return result
    return wrapper

Applying it to a regular method:

class Calculator:
    @log_call
    def add(self, a, b):
        return a + b

Calling calc.add(2, 3) prints the before/after messages while still returning 5. For class methods, the same decorator works because the descriptor automatically supplies cls as the first argument.

Advanced Parameterized Decorators

Sometimes a decorator needs to adapt its behavior based on runtime information, such as the logging level or a cache timeout. The pattern remains the same three‑layer nesting, but the outer layer can capture any configuration data:

def cache_for(seconds):
    def decorator(func):
        cache = {}
        def wrapper(*args, **kwargs):
            key = (args, frozenset(kwargs.items()))
            if key not in cache or (time.time() - cache['_ts']) > seconds:
                result = func(*args, **kwargs)
                cache['result'] = result
                cache['_ts'] = time.time()
            return cache['result']
        return wrapper
    return decorator

Now @cache_for(10) creates a ten‑second memoization cache for the decorated function.

Common Pitfalls and Best Practices

  1. Preserving the original signaturefunctools.wraps copies the __name__, __doc__, and annotation information, but it does not automatically adjust the signature for wrappers that accept *args/**kwargs. For more precise control, functools.update_wrapper can be used, or third‑party libraries like wrapt can be employed Small thing, real impact. Simple as that..

  2. Avoiding side effects – Decorators should not mutate global state unless that is explicitly part of their contract. Prefer returning a new object or a transformed version of the input rather than altering external variables in place Worth keeping that in mind..

  3. Handling exceptions – A well‑behaved decorator should let exceptions propagate unless it explicitly catches and transforms them. Wrapping the call in a try/except block is optional and should be documented.

  4. Maintaining idempotence – Applying a decorator multiple times should not break the function. Take this case: stacking @retry(2) on a function that already has a caching decorator should still work because each layer respects *args/**kwargs.

  5. Documenting intent – A decorator’s purpose should be evident from its name and docstring. This helps other developers understand the contract without digging into the implementation.

Conclusion

Decorators are a powerful metaprogramming tool in Python. That said, by mastering the three‑layer nesting for argument‑aware decorators, preserving function metadata with functools. Now, wraps, and respecting the descriptor protocol for methods, you can build reusable, expressive enhancements that keep code clean and maintainable. Think about it: whether you are adding logging, caching, validation, or enriching classes with metadata, the patterns outlined above provide a solid foundation. Applying these best practices ensures that your decorators remain predictable, composable, and easy to debug, ultimately leading to more solid and readable Python applications Small thing, real impact..

Fresh Out

New Stories

Close to Home

On a Similar Note

Thank you for reading about What Is A Decorator In Python. We hope the information has been useful. Feel free to contact us if you have any questions. See you next time — don't forget to bookmark!
⌂ Back to Home