Fundamentals 18 min read

Mastering Python Decorators: From Basics to Advanced Use Cases

This article explains Python decorators—functions that return functions—to decouple logic, improve reusability, and extend behavior without modifying original code, covering basic syntax, parameterized and nested decorators, functools utilities, built‑in decorators, and practical examples like logging, timing, permission checks, and caching.

Subtle Storm
Subtle Storm
Subtle Storm
Mastering Python Decorators: From Basics to Advanced Use Cases

What is a Decorator?

In Python, functions are first‑class objects, meaning they can be assigned, passed as arguments, and returned. A decorator is a function that returns a function . Using the @ syntax, a function is passed to another function (the decorator) which returns a new function that replaces the original.

Why Use Decorators?

Add behavior before or after a function call.

Modify a function’s input or output.

Attach metadata to a function.

Common scenarios include logging, permission checks, performance timing, and caching. Decorators promote code reuse, maintainability, separation of concerns, and flexibility.

2.1 Basic Structure

def decorator(func):
    def wrapper():
        print("Before function call")
        func()  # call original function
        print("After function call")
    return wrapper

@decorator

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

say_hello()

Explanation: decorator receives func as a parameter. wrapper adds custom behavior before and after calling func(). @decorator is equivalent to say_hello = decorator(say_hello).

Output:

Before function call
Hello!
After function call

2.2 Parameterized Decorator

def decorator(func):
    def wrapper(*args, **kwargs):
        print("Before function call")
        result = func(*args, **kwargs)
        print("After function call")
        return result
    return wrapper

@decorator

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

result = add(3, 5)
print("Result:", result)

Explanation: wrapper forwards *args and **kwargs to the original function, allowing any signature.

The result of func is returned unchanged.

Output:

Before function call
After function call
Result: 8

2.3 Nested Decorators

def decorator1(func):
    def wrapper(*args, **kwargs):
        print("Decorator 1 - Before function call")
        result = func(*args, **kwargs)
        print("Decorator 1 - After function call")
        return result
    return wrapper

def decorator2(func):
    def wrapper(*args, **kwargs):
        print("Decorator 2 - Before function call")
        result = func(*args, **kwargs)
        print("Decorator 2 - After function call")
        return result
    return wrapper

@decorator1
@decorator2

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

say_hello()

Explanation:

Decorators are applied from bottom to top: @decorator2 first, then @decorator1.

Calling say_hello() triggers decorator2 then decorator1.

Output:

Decorator 2 - Before function call
Decorator 1 - Before function call
Hello!
Decorator 1 - After function call
Decorator 2 - After function call

2.4 Preserving Metadata with functools.wraps

import functools

def decorator(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        print("Before function call")
        return func(*args, **kwargs)
    return wrapper

@decorator
def say_hello():
    """This is the say_hello function."""
    print("Hello!")
    print(say_hello.__name__)   # prints: say_hello
    print(say_hello.__doc__)    # prints: This is the say_hello function.
@functools.wraps

ensures the wrapper keeps the original function’s name and docstring.

2.5 Decorators with Arguments (Three‑Level Nesting)

def decorator_with_args(arg):
    def decorator(func):
        def wrapper(*args, **kwargs):
            print(f"Decorator argument: {arg}")
            return func(*args, **kwargs)
        return wrapper
    return decorator

@decorator_with_args("Hello")
def greet(name):
    print(f"Greetings, {name}!")

greet("Alice")

Explanation: decorator_with_args receives a parameter and returns the actual decorator.

The inner decorator receives the target function.

Calling greet prints the decorator argument before executing the function.

Output:

Decorator argument: Hello
Greetings, Alice!

2.6 Decorating Class, Instance, and Static Methods

When decorating methods, the wrapper must accept self (instance) or cls (class) as the first argument.

class MyClass:
    def decorator(func):
        def wrapper(self, *args, **kwargs):
            print("Before calling instance method")
            result = func(self, *args, **kwargs)
            print("After calling instance method")
            return result
        return wrapper

    @decorator
    def greet(self, name):
        print(f"Hello, {name}!")

obj = MyClass()
obj.greet("Alice")

Static‑method example:

class MyClass:
    @staticmethod
    def decorator(func):
        def wrapper(*args, **kwargs):
            print("Before calling static method")
            result = func(*args, **kwargs)
            print("After calling static method")
            return result
        return wrapper

    @decorator
    @staticmethod
    def greet(name):
        print(f"Hello, {name}!")

MyClass.greet("Alice")

3 Built‑in Decorators

@staticmethod

– defines a static method that does not receive self or cls. @classmethod – defines a class method that receives cls as the first argument. @property – turns a method into a read‑only attribute. @functools.lru_cache – caches function results to avoid recomputation. @functools.wraps – preserves original metadata when writing custom decorators. @abstractmethod – marks a method as abstract in an abstract base class. @property.setter – defines a setter for a property. @property.deleter – defines a deleter for a property.

Examples

Static method:

class MyClass:
    @staticmethod
    def greet(name):
        print(f"Hello, {name}!")

MyClass.greet("Alice")

Class method:

class MyClass:
    count = 0
    @classmethod
    def increment_count(cls):
        cls.count += 1
        print(f"Count: {cls.count}")

MyClass.increment_count()

Property and setter:

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

    @property
    def radius(self):
        return self._radius

    @radius.setter
    def radius(self, value):
        if value <= 0:
            raise ValueError("Radius must be positive.")
        self._radius = value

c = Circle(5)
print(c.radius)
c.radius = 10
print(c.radius)

LRU cache:

import functools

@functools.lru_cache(maxsize=None)
def fibonacci(n):
    if n < 2:
        return n
    return fibonacci(n-1) + fibonacci(n-2)

print(fibonacci(35))

4 Real‑World Use Cases

Logging

def log_decorator(func):
    def wrapper(*args, **kwargs):
        print(f"Calling {func.__name__} with args {args} and kwargs {kwargs}")
        result = func(*args, **kwargs)
        print(f"{func.__name__} returned {result}")
        return result
    return wrapper

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

@log_decorator
def multiply(a, b):
    return a * b

add(1, 2)
multiply(3, 4)

Output shows function calls and results, keeping logging separate from business logic.

Performance Monitoring

import time

def time_decorator(func):
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        end = time.time()
        print(f"Function {func.__name__} took {end - start} seconds")
        return result
    return wrapper

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

@time_decorator
def fast_function():
    time.sleep(0.5)

slow_function()
fast_function()

Permission Validation

def permission_required(func):
    def wrapper(*args, **kwargs):
        if not has_permission():
            raise PermissionError("You do not have permission to access this resource.")
        return func(*args, **kwargs)
    return wrapper

def has_permission():
    return False

@permission_required
def sensitive_data():
    return "This is sensitive data."

try:
    sensitive_data()
except PermissionError as e:
    print(e)

Caching Expensive Calls

from functools import lru_cache

@lru_cache(maxsize=None)
def expensive_function(x):
    print(f"Calculating {x}...")
    return x * 2

print(expensive_function(4))  # first call, computes
print(expensive_function(4))  # second call, cached

Decorators in Python provide a powerful, flexible way to modify or extend function and method behavior while keeping core logic clean and maintainable.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

PerformancePythoncachingloggingfunctionsdecoratorsfunctools
Subtle Storm
Written by

Subtle Storm

The micro era's marvels are boundlessly subtle.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.