Skip to content

Decorators

Decorators let you change or add extra behavior to a function or class without actually touching its original code.

They’re used a lot in real projects for things like:

  • Logging
  • Authentication
  • Caching
  • Validation
  • Tracking how fast something runs

Think of a decorator as something that wraps around a function, like a layer on top of it.

graph TD A[Call Function] --> B[Decorator Wrapper] B --> C[Run Code Before] C --> D[Original Function] D --> E[Run Code After] E --> F[Return Result]

A decorator is really just:

  • A function
  • That takes another function as input
  • And gives back a new function
def decorator(func):
def wrapper():
# before
func()
# after
return wrapper
import functools
def my_decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print("Before function runs")
result = func(*args, **kwargs)
print("After function runs")
return result
return wrapper
@my_decorator
def greet():
print("Hello!")
greet()

What’s Actually Happening Behind the Scenes

Section titled “What’s Actually Happening Behind the Scenes”
greet = my_decorator(greet)

Without using wraps, you lose some important details, like:

  • The function’s name
  • Its docstring
  • The help info that shows up when you look it up
import functools
def my_decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@my_decorator
def greet():
"""This function greets the user"""
print("Hello")
print(greet.__name__) # greet
print(greet.__doc__) # This function greets the user

A good decorator should be able to work with any function, no matter what arguments it takes.

That’s why we use:

*args # positional arguments
**kwargs # keyword arguments
import functools
def log_calls(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__}")
result = func(*args, **kwargs)
print(f"{func.__name__} finished")
return result
return wrapper
@log_calls
def add(a, b):
return a + b
print(add(2, 3))

Sometimes you want to customize how a decorator behaves.

To do that, you need to add one more layer around it.

def decorator_with_args(config):
def decorator(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
return decorator
import functools
def repeat(times):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
result = None
for _ in range(times):
result = func(*args, **kwargs)
return result
return wrapper
return decorator
@repeat(3)
def greet(name):
print(f"Hello {name}")
greet("Asha")

Decorators can be used on classes too, not just functions.

They take the class as input and can make changes to it.

def add_status(cls):
cls.status = "ready"
return cls
@add_status
class Task:
pass
print(Task.status) # ready

Where You’ll Actually Use This in Real Projects

Section titled “Where You’ll Actually Use This in Real Projects”

Some common uses:

  • Keeping a log of function calls
  • Checking if someone has permission to do something
  • Measuring how long something takes to run
  • Saving (caching) results so you don’t have to calculate them again
  • Checking that inputs are valid

Example: Measuring How Long a Function Takes

Section titled “Example: Measuring How Long a Function Takes”
import functools
import time
def time_it(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
end = time.time()
print(f"{func.__name__} took {end - start:.4f} seconds")
return result
return wrapper
@time_it
def slow_function():
time.sleep(1)
slow_function()