Programming

Python Decorators Tutorial: Examples, Use Cases & Best Practices

Python decorators let you modify a function’s behavior without changing its code. For example, you can just use @decorator syntax to add logging or timing to any function.

In this tutorial, you’ll learn how decorators work, see some real-world code examples and know about some best practices to follow. If you’re new to decorators, buckle up; by the end of reading this article, you can use them like a pro.

Tip💡: Explore more advanced topics in Python like this.

What Are Decorators in Python?

Think of decorators as fancy gift wrappers for your functions. They allow you to take an existing function and extend its behaviour without modifying its source code. It’s like adding superpowers to your functions on demand!

In Python terms, decorators are just functions that modify other functions. They’re like powerful middleware that can execute code before and after the wrapped function runs. At a high level, decorators follow the decorator design pattern: they take a function, add some behaviour, and then return it.

In Python, they’re compelling because functions are first-class citizens, meaning you can pass them around and manipulate them like any other object.

Your First Decorator: Baby Step

Let’s start with something straightforward:

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

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

# When we call say_hello()...
say_hello()
# Output:
# Something is happening before the function
# Hello!
# Something is happening after the functionCode language: PHP (php)

💡 The @ syntax is just syntactic sugar! It’s the same as writing: say_hello = my_first_decorator(say_hello)

The Anatomy of a Python Decorator

Let’s break down the previous example and dive into step by step what’s happening:

  1. We define a function (my_first_decorator) that takes another function as an argument.
  2. Inside, we define a new function (wrapper) that calls the original function and modifies its result.
  3. We return this new wrapper function.
  4. When we use the @my_first_decorator Syntax, Python essentially does this behind the scenes:

Core Decorator Types

Function Decorators

These are the most common type of decorators. They are used to modify or enhance functions or methods. A function decorator is a higher-order function that takes a function as an argument and returns a new function (or a modified version of the original).

Key Characteristics

  • Can modify arguments or return values
  • Wraps a single function or method.
  • Can add functionality before or after the original function’s execution.

Example:

Refer to the very first example we shared earlier in this guide.

Class Decorators: The Object-Oriented Approach

Class decorators are used to modify or enhance entire classes. A class decorator is a function that takes a class as an argument and returns a new class or a modified version of the original class.

Key Characteristics

  • Can be used for tasks like singleton creation or automatic registration of classes.
  • Wraps an entire class.
  • Can add or modify class attributes or methods.

Example:

class Cache
    def __init__(self):
        self.data = {}
    
    def __call__(self, func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            key = str(args) + str(kwargs)
            if key not in self.data:
                self.data[key] = func(*args, **kwargs)
            return self.data[key]
        return wrapper

@Cache()
def expensive_computation(n):
    print("Computing...")
    return n * n

print("First try:")
print(expensive_computation(10))
print("Second attempt:")
print(expensive_computation(10))

# Output
# First try:
# Computing...
# 100
# Second attempt:
# 100

Built-in Decorators

Python comes with several built-in decorators that provide common functionalities. These are often used to interact with classes and methods in specific ways.

@staticmethod decorator

Declares a static method within a class. Static methods don’t receive an implicit first argument (self or cls). They are bound to the class and not the instance of the class.

class MathUtils:
    @staticmethod
    def add(x, y):
        return x + y

print(MathUtils.add(5, 3)) # Output: 8Code language: CSS (css)

@classmethod decorator

Declares a class method. Class methods receive the class itself as the first argument, conventionally named cls. They can modify class state that applies across all instances of the class.

class Car:
    num_wheels = 4

    @classmethod
    def get_num_wheels(cls):
        return cls.num_wheels

print(Car.get_num_wheels()) # Output: 4

@property decorator

Used to create getter, setter, and deleter methods for a class attribute, allowing you to treat a method like an attribute.

class Circle:
    def __init__(self, radius):
        self._radius = radius

    @property
    def radius(self):
        print("Getting radius")
        return self._radius

    @radius.setter
    def radius(self, value):
        if value < 0:
            raise ValueError("Radius cannot be negative")
        print("Setting radius")
        self._radius = value

c = Circle(5)
print(c.radius) # Calls the getter
c.radius = 10   # Calls the setterCode language: HTML, XML (xml)

Levelling Up – Python Decorators with Arguments!

Python decorators can take arguments if you want it to. Here’s a simple example:

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

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

greet("Bob")
# Output:
# Hello, Bob
# Hello, Bob
# Hello, BobCode language: PHP (php)

Real-World Examples Of Python Decorators

Authentication Decorator

This is similar to what we can use to implement auth gatekeeping to some functions:

def require_auth(func):
    @wraps(func)
    def wrapper(request, *args, **kwargs):
        auth_token = request.headers.get('Authorization')
        if not auth_token:
            raise AuthError("No auth token provided")
        if not is_valid_token(auth_token):
            raise AuthError("Invalid token")
        return func(request, *args, **kwargs)
    return wrapper

@require_auth
def get_user_data(request):
    return {"user": "data"}Code language: JavaScript (javascript)

Decorator For Retry Mechanism:

Here’s another real-world use case for retry logic. It’s often useful in scenarios like microservices where networks can often be unreliable:

def retry(max_attempts=3, delay=1):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            attempts = 0
            while attempts < max_attempts:
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    attempts += 1
                    if attempts == max_attempts:
                        raise e
                    time.sleep(delay)
            return None
        return wrapper
    return decorator

@retry(max_attempts=3, delay=2)
def unstable_network_call():
    # Simulated unstable API call
    passCode language: PHP (php)

Timing Decorator:

Here’s something I use all the time to profile my functions and optimize:

import time
from functools import wraps

def timer(func):
    @wraps(func)  # This preserves the original function's metadata
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        end = time.time()
        print(f"{func.__name__} took {end - start:.2f} seconds to run")
        return result
    return wrapper

@timer
def slow_function():
    """This is a slow function."""
    time.sleep(1)
    return "Done!"

print(slow_function())
# Output:
# slow_function took 1.00 seconds to run
# Done!Code language: PHP (php)

⚠️ Common Mistake Alert: Forgetting @wraps(func) will mess up your function’s metadata, causing headaches with testing and documentation tools.

Common Pitfalls And How to Avoid Them

Mutable Arguments in Decorators:

# DON'T DO THIS
def decorator(func):
    cache = []  # This is shared between all decorated functions!
    def wrapper():
        return func()
    return wrapper

# DO THIS
def decorator(func):
    def wrapper(cache=None):
        cache = cache or []  # New cache for each call
        return func()
    return wrapperCode language: PHP (php)

Decorators Order Matter in Python:

@decorator1
@decorator2
def function():
    pass

# Is equivalent to:
function = decorator1(decorator2(function))
# decorator1 executes after decorator2Code language: PHP (php)

Forgetting to Return the Wrapper:

If you forget to return wrapper in your decorator, it’ll break, likely with a NoneType error.

# Broken example - missing return statement
def broken_decorator(func):
    def wrapper():
        print("Doing something extra!")
        func()

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

# This will raise an error when called
# greet()Code language: PHP (php)

Python Decorators Best Practices

You probably picked up a few ones already if you followed thoroughly so far. However, here’s a concise list of key best practices when working with decorators:

  • Always use @wraps , to help preserve metadata
  • Keep decorators focused on a single responsibility
  • Document what your decorators do
  • Consider making decorators optional with parameters
  • Test decorated functions both with and without the decorator

Conclusion

Decorators are like secret ingredients in your Python cookbook. They allow you to write cleaner, more modular code by separating concerns. Remember, the key to mastering decorators is practice. Start small, experiment, and soon you’ll be wrapping functions like a pro! Also, read more on official documentaiton as well.

Happy Python programming! 🐍✨

Rana Ahsan

Rana Ahsan is a seasoned software engineer and technology leader specialized in distributed systems and software architecture. With a Master’s in Software Engineering from Concordia University, his experience spans leading scalable architecture at Coursera and TopHat, contributing to open-source projects. This blog, CodeSamplez.com, showcases his passion for sharing practical insights on programming and distributed systems concepts and help educate others. Github | X | LinkedIn

Recent Posts

Advanced Service Worker Features: Push Beyond the Basics

Unlock the full potential of service workers with advanced features like push notifications, background sync, and performance optimization techniques that transform your web app into…

6 days ago

Service Workers in React: Framework Integration Guide

Learn how to integrate service workers in React, Next.js, Vue, and Angular with practical code examples and production-ready implementations for modern web applications.

3 weeks ago

Service Worker Caching Strategies: Performance & Offline Apps

Master the essential service worker caching strategies that transform web performance. Learn Cache-First, Network-First, and Stale-While-Revalidate patterns with practical examples that'll make your apps blazingly…

4 weeks ago

This website uses cookies.