PYTHON

Python Decorator: Syntax, Usage, and Examples

A Python decorator lets you modify or extend the behavior of a function or class without changing its actual code. You wrap the function inside another function to add extra functionality like logging, authentication, or performance tracking.

How to Use a Decorator in Python

A decorator in Python follows a simple structure:

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

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

say_hello()

How Decorators Work

  • The my_decorator function takes another function (func) as an argument.
  • Inside it, wrapper() runs extra code before and after calling func().
  • When you use @my_decorator before say_hello(), it automatically passes say_hello() into my_decorator() and replaces it with wrapper().

Using Decorators with Function Arguments

Functions often take arguments, and decorators need to handle them too. You can use *args and **kwargs to make sure the decorator works with any function.

def log_function_call(func):
    def wrapper(*args, **kwargs):
        print(f"Calling function {func.__name__} with arguments {args} {kwargs}")
        return func(*args, **kwargs)
    return wrapper

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

print(add(3, 5))  # Output: Calling function add with arguments (3, 5) {} \n 8

When to Use Python Decorators

Decorators are useful when you need to:

  1. Modify function behavior without changing its code
    • Example: Automatically logging function calls.
  2. Reuse code across multiple functions
    • Example: Enforcing authentication in web applications.
  3. Improve readability and maintainability
    • Example: Making functions cleaner by handling repetitive logic separately.

Examples of Python Decorators

Measuring Execution Time

A timer decorator lets you see how long a function takes to run.

import time

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

@timer
def slow_function():
    time.sleep(2)

slow_function()  # Output: slow_function executed in 2.000xx seconds

Passing Arguments to a Decorator

Sometimes, you want to customize how a decorator works. Instead of modifying the decorator function directly, you can use an extra function level.

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

@repeat(3)
def greet():
    print("Hello!")

greet()  # Output: "Hello!" printed 3 times

Using a Class as a Decorator

You can also create decorators using classes. This approach gives you more flexibility, especially when you need to store state between function calls.

class CountCalls:
    def __init__(self, func):
        self.func = func
        self.count = 0

    def __call__(self, *args, **kwargs):
        self.count += 1
        print(f"{self.func.__name__} has been called {self.count} times")
        return self.func(*args, **kwargs)

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

say_hello()
say_hello()

Learn More About Python Decorators

The @property Decorator

The @property decorator lets you define getter methods without explicitly calling them. You use it to control how a class’s attributes are accessed.

class Person:
    def __init__(self, name):
        self._name = name

    @property
    def name(self):
        return self._name

p = Person("Alice")
print(p.name)  # Output: Alice

The @lru_cache Decorator (Memoization)

Python’s functools.lru_cache decorator caches results so that repeated function calls with the same arguments don’t recompute the results.

from functools import lru_cache

@lru_cache(maxsize=3)
def fib(n):
    if n < 2:
        return n
    return fib(n-1) + fib(n-2)

print(fib(10))  # Output: 55

Using a Lambda Function in a Decorator

You can simplify some decorators with lambda functions. This is useful for short, single-expression decorators.

def double_return(func):
    return lambda *args, **kwargs: func(*args, **kwargs) * 2

@double_return
def square(x):
    return x * x

print(square(3))  # Output: 18 (3*3, doubled)

A Custom Cache Decorator

Instead of using @lru_cache, you can build your own caching decorator.

def cache_decorator(func):
    cache = {}

    def wrapper(*args):
        if args in cache:
            print("Fetching from cache")
            return cache[args]
        result = func(*args)
        cache[args] = result
        print("Storing in cache")
        return result
    return wrapper

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

print(add(2, 3))  # Output: Storing in cache \n 5
print(add(2, 3))  # Output: Fetching from cache \n 5

Using Decorators for Authentication

In web applications, decorators help enforce authentication.

def requires_login(func):
    def wrapper(user):
        if not user.get("logged_in"):
            print("Access Denied: Please log in")
            return
        return func(user)
    return wrapper

@requires_login
def view_profile(user):
    print(f"Welcome, {user['name']}!")

user1 = {"name": "Alice", "logged_in": True}
user2 = {"name": "Bob", "logged_in": False}

view_profile(user1)  # Output: Welcome, Alice!
view_profile(user2)  # Output: Access Denied: Please log in

Chaining Multiple Decorators

You can stack multiple decorators on a single function. Python applies them from bottom to top.

def uppercase(func):
    def wrapper():
        return func().upper()
    return wrapper

def exclaim(func):
    def wrapper():
        return func() + "!"
    return wrapper

@uppercase
@exclaim
def greet():
    return "hello"

print(greet())  # Output: HELLO!

Best Practices When Using Decorators

  • Use functools.wraps()

    When defining a decorator, use @functools.wraps(func) to preserve the original function’s name and docstring.

    from functools import wraps
    
    def my_decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            return func(*args, **kwargs)
        return wrapper
    
  • Keep decorators simple

    If a decorator becomes too complex, consider refactoring it into multiple functions or a class.

  • Document your decorators

    If you write custom decorators for a project, add comments or docstrings to explain what they do.

Python decorators let you modify functions dynamically, making them powerful tools for writing cleaner, reusable code. You can use them for logging function calls, enforcing security, or improving performance, decorators help you keep your code DRY (Don't Repeat Yourself) while adding flexibility.

Learn to Code in Python for Free
Start learning now
button icon
To advance beyond this tutorial and learn Python by doing, try the interactive experience of Mimo. Whether you're starting from scratch or brushing up your coding skills, Mimo helps you take your coding journey above and beyond.

Sign up or download Mimo from the App Store or Google Play to enhance your programming skills and prepare for a career in tech.

You can code, too.

© 2025 Mimo GmbH