← Back to Blogs

Python Decorators Explained: From Basics to Advanced Patterns

By CoilPad • January 1, 2026 · 10 min read

Decorators are one of Python's most powerful and distinctive features. They allow you to modify or enhance the behavior of functions or classes without directly changing their source code.

While they can look intimidating at first (what's with the @ symbol?), they are surprisingly simple once you understand that they are just functions that take other functions as input.

What is a Decorator?

At its core, a decorator is a wrapper. It takes a function, adds some functionality before or after it runs, and returns the modified function.

The Basic Syntax

Here is the manual way to apply a decorator versus the syntactic sugar using the @ symbol. Both do exactly the same thing.

# The manual way
def my_decorator(func):
    def wrapper():
        print("Before function call")
        func()
        print("After function call")
    return wrapper

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

say_hello = my_decorator(say_hello)

# The Pythonic way (Syntactic Sugar)
@my_decorator
def say_hello():
    print("Hello!")

Real-World Examples

Let's look at three practical scenarios where decorators make your code cleaner and more efficient.

1. Timing Execution

A common use case is measuring how long a function takes to run. Instead of adding timing code to every function, you can write one decorator.

import time
import functools

def timer(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        start_time = time.perf_counter()
        result = func(*args, **kwargs)
        end_time = time.perf_counter()
        run_time = end_time - start_time
        print(f"Finished {func.__name__!r} in {run_time:.4f} secs")
        return result
    return wrapper

@timer
def heavy_computation():
    sum([i**2 for i in range(1000000)])

heavy_computation()
# Output: Finished 'heavy_computation' in 0.3452 secs

2. Retry Logic

When dealing with network requests or APIs, failures happen. A retry decorator can automatically try the operation again if it fails.

import random
import time

def retry(max_attempts=3, delay=1):
    def decorator(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
                    print(f"Attempt {attempts} failed. Retrying...")
                    time.sleep(delay)
        return wrapper
    return decorator

@retry(max_attempts=3)
def unstable_network_call():
    if random.random() < 0.7:
        raise ConnectionError("Network fail")
    return "Success!"

3. Authentication Check

In web frameworks like Flask or Django, decorators are often used to enforce permissions.

def require_admin(func):
    def wrapper(user, *args, **kwargs):
        if not user.get('is_admin'):
            raise PermissionError("User must be an admin to access this resource")
        return func(user, *args, **kwargs)
    return wrapper

@require_admin
def delete_database(user):
    print("Database deleted!")

user = {'username': 'navin', 'is_admin': False}
# delete_database(user)  # Raises PermissionError

Built-in Decorators

Python comes with several useful decorators built-in.

  • @property: Allows you to access a method like an attribute.
  • @staticmethod: Defines a method that doesn't need access to the instance (self) or class (cls).
  • @classmethod: Defines a method that receives the class (cls) as the first argument instead of the instance.
  • @functools.lru_cache: Automatically memoizes (caches) the results of a function based on its arguments.

Best Practices

When writing your own decorators, keep these tips in mind:

  1. Use functools.wraps: This ensures your decorated function retains its original name and docstring.
  2. Handle *args and **kwargs: Make your wrapper function accept arbitrary arguments so it can work with any function signature.
  3. Keep them focused: Do one thing well. If your decorator is doing logging, timing, and error handling, split it up.

Experiment with Decorators

The best way to learn is by doing. Try creating your own decorators in CoilPad and see how they transform your code instantly.

Download CoilPad