How to Understand and Use Decorator Functions in Python ?

Introduction

Decorators are one of Python’s most powerful—and often confusing—features. At first glance, the @decorator syntax can feel like magic. But once you understand how decorators work, they become an elegant tool for writing cleaner, more reusable code.

What Is a Decorator in Python?

A decorator is a function that:

  • Takes another function as input
  • Extends or modifies its behavior
  • Returns a new function

Key idea: decorators let you add functionality without changing the original function’s code.

In Python, functions are first-class objects, meaning:

  • They can be passed as arguments
  • They can be returned from other functions
  • They can be assigned to variables

Decorators are built on top of this concept.

A Simple Function Example

Let’s start with a basic function:

1
2
def greet():
    print("Hello!")

Calling it:

1
greet()

Output:

1
Hello!

Now suppose we want to:

  • Print something before and after greet() runs
    We could modify the function—but that doesn’t scale well.

This is where decorators shine.

Writing Your First Decorator**

Here’s a simple decorator:

1
2
3
4
5
6
def my_decorator(func):
    def wrapper():
        print("Before the function runs")
        func()
        print("After the function runs")
    return wrapper

Let’s break it down:

  • func is the function being decorated
  • wrapper() adds extra behavior
  • func() calls the original function
  • The decorator returns the new function

Apply it manually:

1
2
3
4
5
def greet():
    print("Hello!")

greet = my_decorator(greet)
greet()

Output:

1
2
3
Before the function runs
Hello!
After the function runs

The @decorator Syntax

Python provides a cleaner syntax using @:

1
2
3
@my_decorator
def greet():
    print("Hello!")

This is equivalent to:

1
greet = my_decorator(greet)

Much cleaner—and easier to read.

Decorators with Arguments

Most real functions accept parameters. Let’s update our decorator:

1
2
3
4
5
6
7
def my_decorator(func):
    def wrapper(*args, **kwargs):
        print("Before function")
        result = func(*args, **kwargs)
        print("After function")
        return result
    return wrapper

Now it works with any function:

1
2
3
4
5
@my_decorator
def add(a, b):
    return a + b

print(add(3, 5))

Output:

1
2
3
Before function
After function
8

Preserving Function Metadata (functools.wraps)

Without extra care, decorators can overwrite:

  • Function name
  • Docstring
  • Annotations

Example problem:

1
print(add.__name__)  # wrapper

Fix it with functools.wraps:

1
2
3
4
5
6
7
from functools import wraps

def my_decorator(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

Now metadata is preserved:

1
print(add.__name__)  # add

Decorators with Arguments (Advanced)

Sometimes decorators themselves need parameters:

1
2
3
4
5
6
7
def repeat(n):
    def decorator(func):
        def wrapper(*args, **kwargs):
            for _ in range(n):
                func(*args, **kwargs)
        return wrapper
    return decorator

Usage:

1
2
3
4
5
@repeat(3)
def say_hi():
    print("Hi!")

say_hi()

Output:

1
2
3
Hi!
Hi!
Hi!

Common Real-World Use Cases

Logging

1
2
3
4
5
def log(func):
    def wrapper(*args, **kwargs):
        print(f"Calling {func.__name__}")
        return func(*args, **kwargs)
    return wrapper

Timing Function Execution

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
import time

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

Authentication / Permissions

Decorators are commonly used in web frameworks like Flask and Django.

1
2
3
4
5
6
def require_login(func):
    def wrapper(user):
        if not user.is_authenticated:
            raise PermissionError("Login required")
        return func(user)
    return wrapper

Built-in Decorators You Already Use

Python includes many decorators out of the box:

  • @staticmethod
  • @classmethod
  • @property
  • @dataclass
  • @lru_cache

@lru_cache decorator in Python

Example:

1
2
3
4
5
6
7
from functools import lru_cache

@lru_cache
def fibonacci(n):
    if n < 2:
        return n
    return fibonacci(n-1) + fibonacci(n-2)

@classmethod decorator in Python

Example of using the @classmethod decorator in Python

Basic example

1
2
3
4
5
6
class MyClass:
    value = 10

    @classmethod
    def show_value(cls):
        return cls.value

How it works

1
print(MyClass.show_value())   # 10
  • @classmethod receives the class itself as the first argument (cls)
  • You don’t need to create an instance
  • It can access class variables (cls.value)

Comparison with instance method

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
class MyClass:
    value = 10

    def instance_method(self):
        return self.value

    @classmethod
    def class_method(cls):
        return cls.value


obj = MyClass()

print(obj.instance_method())  # 10
print(MyClass.class_method()) # 10
  • self → instance
  • cls → class

Practical example: alternative constructor

This is the most common real-world use.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
class FirePixel:
    def __init__(self, lat, lon, frp):
        self.lat = lat
        self.lon = lon
        self.frp = frp

    @classmethod
    def from_dict(cls, data):
        return cls(
            lat=data["latitude"],
            lon=data["longitude"],
            frp=data["frp"]
        )

Usage

1
2
3
4
5
d = {"latitude": 34.2, "longitude": -118.4, "frp": 56.7}

pixel = FirePixel.from_dict(d)

print(pixel.lat, pixel.lon, pixel.frp)

Why @classmethod here?

  • Creates an instance without hardcoding the class name
  • Works correctly if the class is subclassed

Subclassing benefit

1
2
3
4
5
class VIIRSPixel(FirePixel):
    pass

p = VIIRSPixel.from_dict(d)
print(type(p))  # <class '__main__.VIIRSPixel'>

If from_dict were a @staticmethod, this would not work properly.

When to use @classmethod

Use it when:

  • You need access to the class, not an instance
  • You want factory methods / alternative constructors
  • You want code that respects inheritance

When NOT to use it

  • If you only need instance data → use normal method
  • If you don’t need cls → use @staticmethod

When Should You Use Decorators?

Use decorators when you want to:

  • Add cross-cutting behavior (logging, timing, caching)
  • Avoid repeating code
  • Keep business logic clean
  • Apply behavior consistently across functions

Avoid decorators when:

  • The logic is highly specific to one function
  • Readability would suffer

References