When I first saw decorators in Python, they felt a bit strange.
That @ symbol looked unusual.
It felt like some special Python trick that experienced developers understood and beginners were not supposed to touch yet.
At least that’s how it felt to me.
But later I realized something simple.
Decorators are just functions.
A decorator is simply a function that takes another function as input and returns a function.
That’s it.
Nothing magical.
This works because in Python functions are first-class objects.
Which simply means they can be passed around like any other piece of data.
Once this clicked for me, decorators started feeling much more natural.
And actually… quite pleasant to use.
One thing I like about decorators is that they let me hide small repeating patterns.
Instead of writing the same logic again and again, I can wrap that logic in a decorator and reuse it.
A very simple example is keeping track of functions.
Imagine we want to automatically register certain functions.
Instead of manually maintaining a list, we can use a decorator.
registry = []
def register(func):
registry.append(func)
return func
Now we can mark functions like this:
@register
def greet():
print("hello")
@register
def bye():
print("goodbye")
Now those functions automatically appear in the registry.
print(registry)
Output:
[<function greet>, <function bye>]
We didn’t manually add anything.
The decorator handled the pattern for us.
The @ symbol may look special, but it's really just syntax sugar.
This:
@register
def greet():
print("hello")
Is exactly the same as writing:
def greet():
print("hello")
greet = register(greet)
Python simply passes the function into another function.
Once I realized this, decorators stopped feeling mysterious.
They're just a cleaner way to write something we could already do.
Another place decorators are useful is measuring how long a function takes to run.
Instead of writing timing code everywhere, we can hide that logic inside a decorator.
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} seconds")
return result
return wrapper
Now we can use it like this:
@timer
def slow_function():
time.sleep(1)
Calling the function:
slow_function()
Output:
slow_function took 1.0001 seconds
The original function stays clean, and the timing logic lives somewhere reusable.
Sometimes I like to think of decorators like this:
your_function → decorator → new_function
The decorator takes your function, wraps some extra behavior around it, and returns a new function.
The @ syntax just makes this transformation easier to write.
When I started learning Python, decorators looked intimidating.
Now they feel like a small elegant tool.
They let me hide patterns like:
Instead of repeating that logic everywhere.
I write it once.
And then I reuse it.
Decorators felt strange when I first saw them.
But once the idea clicked, I started reaching for them quite often.
And honestly… they’re kind of fun to use.