Decorators
Learn how decorators use higher-order functions to wrap and extend the behavior of other functions without changing their code.
Introduction
Imagine you want to add logging, timing, or permission checks to ten different functions, without rewriting the same three lines of code inside every single one. Decorators solve exactly this problem — they let you wrap extra behavior around a function from the outside.
Decorators can look intimidating the first time you see the @ symbol above a function, but they rest on a concept you already know: functions are just objects that can be passed around, returned, and combined like any other value.
- A quick recap of why functions are first-class objects in Python.
- What a decorator actually is, conceptually.
- How to write a simple decorator using a wrapper function.
- How to apply a decorator using the @decorator syntax.
- A practical timing decorator example.
- Why functools.wraps matters, and a preview of built-in decorators like @property.
Functions as First-Class Objects (Recap)
In Python, functions are first-class objects. That means a function can be assigned to a variable, passed as an argument to another function, and even returned from a function — just like a number or a string.
def greet(name):
return f"Hello, {name}!"
say_hello = greet # assign the function to a variable
print(say_hello("Maya"))
def call_twice(func, value):
print(func(value))
print(func(value))
call_twice(greet, "Sam") # pass a function as an argumentHello, Maya!
Hello, Sam!
Hello, Sam!This ability — passing functions around and even returning new functions from other functions — is the entire foundation that decorators are built on.
What is a Decorator?
A decorator is a function that takes another function as input, and returns a new function that usually calls the original one while adding some extra behavior before, after, or around it.
Wraps Behavior
A decorator adds functionality around an existing function, without modifying its actual source code.
Reusable
The same decorator can be applied to many different functions.
Higher-Order Function
A decorator is simply a function that takes a function and returns a function.
Writing a Simple Decorator
A decorator is usually written with an inner "wrapper" function. The outer function receives the original function; the inner wrapper calls it and adds extra behavior around the call.
def shout(func):
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
return result.upper()
return wrapper
def greet(name):
return f"Hello, {name}!"
loud_greet = shout(greet) # manually decorating
print(loud_greet("world"))HELLO, WORLD!The wrapper function accepts *args and **kwargs so it can forward any combination of arguments to the original function, no matter how it was called. This makes the decorator generic enough to work with almost any function.
Applying a Decorator with @ Syntax
Instead of manually writing greet = shout(greet), Python provides a cleaner syntax: placing @decorator_name directly above the function definition. This does exactly the same thing, just more readably.
def shout(func):
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
return result.upper()
return wrapper
@shout
def greet(name):
return f"Hello, {name}!"
print(greet("world"))HELLO, WORLD!@shout above def greet(...) is exactly equivalent to writing greet = shout(greet) right after the function is defined.
Practical Example: A Timing Decorator
A very common real-world use for decorators is measuring how long a function takes to run, without cluttering the function itself with timing code.
import time
def timer(func):
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
end = time.perf_counter()
print(f"{func.__name__} took {end - start:.4f} seconds")
return result
return wrapper
@timer
def slow_square(n):
total = 0
for i in range(n):
total += i * i
return total
print(slow_square(1000000))slow_square took 0.0521 seconds
333332833333500000Notice how the timing logic lives entirely inside the decorator. The slow_square function itself stays focused purely on its own job — computing a sum of squares — with no timing code mixed in.
def log_call(func):
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__} with args={args}, kwargs={kwargs}")
result = func(*args, **kwargs)
print(f"{func.__name__} returned {result}")
return result
return wrapper
@log_call
def add(a, b):
return a + b
add(3, 4)Calling add with args=(3, 4), kwargs={}
add returned 7functools.wraps
There is a subtle downside to decorators: the wrapper function replaces the original one, so things like the function's name and docstring get hidden behind the wrapper's own metadata.
def shout(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs).upper()
return wrapper
@shout
def greet(name):
"""Return a friendly greeting."""
return f"Hello, {name}!"
print(greet.__name__)
print(greet.__doc__)wrapper
NoneThis is fixed with functools.wraps, a small decorator you apply to the wrapper function itself. It copies over the original function's name, docstring, and other metadata automatically.
import functools
def shout(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs).upper()
return wrapper
@shout
def greet(name):
"""Return a friendly greeting."""
return f"Hello, {name}!"
print(greet.__name__)
print(greet.__doc__)greet
Return a friendly greeting.Without functools.wraps, tools like help(), debuggers, and documentation generators will show confusing information about a generic "wrapper" instead of your actual function.
Built-in Decorators (Forward Reference)
Python's standard library ships several decorators you will use constantly once you start writing classes. They are covered in full detail in upcoming lessons, but it helps to recognize them now.
@staticmethod
Marks a method that does not need access to the instance (self) or the class — it behaves like a plain function living inside a class.
@classmethod
Marks a method that receives the class itself (cls) instead of an instance, often used for alternate constructors.
@property
Lets you access a method like an attribute (without parentheses), commonly used to create computed, read-only values.
Common Mistakes
- Forgetting *args and **kwargs in the wrapper, which breaks decorators applied to functions with different signatures.
- Forgetting to return the result of func(*args, **kwargs) inside the wrapper, silently swallowing the return value.
- Skipping functools.wraps, which hides the original function's name and docstring.
- Confusing @decorator (a name, no parentheses) with @decorator() (calling a function that returns a decorator) — mixing these up causes TypeErrors.
Best Practices
- Always accept *args and **kwargs in your wrapper so the decorator works with any function signature.
- Always return the wrapped function's result unless you deliberately intend to change it.
- Always apply @functools.wraps(func) to the inner wrapper function.
- Keep decorators focused on one concern each (timing, logging, validation) rather than combining unrelated behavior.
- Give decorators clear, descriptive names that explain what behavior they add.
Frequently Asked Questions
Can I apply more than one decorator to the same function?
Yes. You can stack multiple decorators, one per line, above a function. They apply from the bottom up — the decorator closest to the function runs first.
Do decorators work on functions with arguments?
Yes, as long as the wrapper function accepts *args and **kwargs and forwards them to the original function when it calls it.
Is a decorator only for functions?
Decorators are most commonly applied to functions and methods, but classes can be decorated too, and this pattern is used by some advanced libraries.
What is the difference between @property and a regular decorator I write myself?
@property is a built-in decorator specifically designed for use inside classes, letting a method be accessed like a plain attribute. It is covered in detail in the classes lessons.
Why not just call the decorator function manually instead of using @?
You can — @decorator above a function is just syntactic sugar for func = decorator(func). The @ syntax is simply the conventional, more readable way to write it.
Key Takeaways
- Functions in Python are first-class objects — they can be assigned, passed around, and returned like any other value.
- A decorator is a function that takes a function and returns a new, wrapped function.
- The @decorator syntax above a function definition is shorthand for func = decorator(func).
- A wrapper function should accept *args and **kwargs so it works with any function signature.
- functools.wraps preserves the original function's name and docstring on the wrapped version.
- @staticmethod, @classmethod, and @property are built-in decorators used heavily with classes, covered in upcoming lessons.
Summary
Decorators let you cleanly add reusable behavior — like timing, logging, or validation — around existing functions, without modifying their internal code. They rely on the fact that functions are ordinary objects that can be passed into other functions and wrapped.
In this lesson, you learned how to write and apply a decorator, saw practical timing and logging examples, learned why functools.wraps matters, and previewed the built-in decorators you will meet in upcoming class-based lessons. Next, you will learn about namespaces — how Python organizes and looks up names internally.
- You understand what a decorator is and how it wraps a function.
- You can write, apply, and stack your own decorators.
- You know why functools.wraps is important.
- You are ready to learn about namespaces.