What is a Python decorator at its most basic level?
The term "Decorator" comes from the Decorator Design Pattern. It allows you to "wrap" an existing piece of code in a new layer of logic.
Think of it like a Gift Wrap:
The Function is the gift inside.
The Decorator is the wrapping paper and the bow.
When you receive the gift, you still get the item inside, but the appearance and presentation have been modified.
Key Point: Decorators allow you to follow the DRY (Don't Repeat Yourself) principle by separating "boilerplate" code (like logging or security checks) from the main logic.
Which symbol is used as "Syntactic Sugar" to apply a decorator to a function in Python?
While you can apply decorators manually, Python provides the @ symbol to make your code much cleaner and easier to read.
# This syntax:
@my_decorator
def my_function():
pass
# Is exactly the same as:
def my_function():
pass
my_function = my_decorator(my_function)
The @ symbol is placed immediately above the function definition you wish to modify.
In Python, what does it mean that functions are "First-Class Objects"?
This is the foundation of all advanced Python features. If functions weren't first-class objects, decorators wouldn't be possible!
What you can do with a function:
Assignment:say_hi = print -> say_hi("Hello")
Pass as Argument: You can send a function into another function (this is what decorators do).
Nested Functions: You can define a function inside another function.
Look at this code snippet. What is the role of wrapper in this decorator pattern?
When you decorate a function, you are effectively "swapping" the original function for the wrapper.
The Substitution:
The decorator takes the original func.
It defines a new function (the wrapper) that calls func() inside it.
It returns the wrapper.
From that point on, every time you call the decorated function, you are actually running the wrapper logic!
What happens to the output of a function if the decorator's wrapper function forgets to call the original func()?
This is a common bug when writing your first decorator.
Because the decorator replaces your function with the wrapper, if you don't explicitly call func() inside that wrapper, the original code inside your function will simply be skipped. The wrapper has complete control over whether the original function runs at all.
Tip: This "control" is actually a feature! For example, an @admin_only decorator might choose not to call the original function if the user doesn't have the right permissions.
You want to create a decorator that logs every time a function is called, but the functions it decorates have different numbers of arguments (e.g., add(a, b) vs greet(name)). Which signature should the
To make a decorator "generic" (able to wrap any function), you must use argument unpacking.
Why *args and **kwargs?
*args: Captures all positional arguments as a tuple.
**kwargs: Captures all keyword arguments as a dictionary.
By using wrapper(*args, **kwargs) and then passing them into the original function as func(*args, **kwargs), the decorator acts like a transparent pipe, allowing any data to pass through without needing to know the function's specific signature.
What is wrong with the following decorator?
def double_result(func):
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
result * 2
return wrapper
@double_result
def add(a, b):
return a + b
print(add(5, 5))
This is the most frequent mistake in intermediate decorator development.
The "Middleman" Rule:
Since the wrapper replaces the original function, the caller (the print statement) is now getting the output of the wrapper, not the original add function.
Inside the wrapper, result * 2 is calculated, but it is never returned to the caller.
In Python, a function that doesn't explicitly return something returns None by default.
Correct Fix:return result * 2 inside the wrapper.
You are building a web app and want to create a decorator @timer that measures how many seconds a function takes to execute. Which implementation is correct?
This is a standard "Boilerplate" decorator used in performance tuning.
Why Option 1 is correct:
It captures the start time.
It executes the original function and saves the result.
It calculates the difference and prints it.
It returns the original result so the rest of the program continues to work normally.
This decorator modifies the output of the function.
Step-by-Step:
greet() is called, which actually triggers the wrapper.
The wrapper calls the original greet, which returns "hello".
The wrapper then applies .upper() and adds "!!!".
The final result returned to the print statement is "HELLO!!!".
You want a decorator that only allows a function to run if a global variable USER_LOGGED_IN is True. If False, it should print "Access Denied". Which logic fits best?
This is a Conditional Execution decorator.
The wrapper acts as a gatekeeper. By using an if/else statement, it decides whether to execute the original func or to skip it and show an error message instead. This is a very common pattern in frameworks like Flask or Django for protecting specific URLs.
When you decorate a function, it technically becomes the wrapper function. If you check print(greet.__name__) after decorating it, it will say "wrapper" instead of "greet". What is the standard Pythonic way to fix this?
Decorators "hide" the original function's metadata (its name, docstring, and argument list). This can break debugging tools and help documentation.
The Solution: functools.wraps
Python provides a built-in decorator for your decorators! By using @wraps(func), Python copies all the original metadata from func onto your wrapper.
This exercise combines Decorator Factories and Stacking.
Step-by-Step Logic:
@tag("b") wraps greet, resulting in Hello.
@tag("p") wraps the entire result of the first step.
Final output: <p><b>Hello</b></p>.
The decorator closest to the function (the bottom one) is the "inner" layer, and the top decorator is the "outer" layer.
How can a function-based decorator maintain a persistent counter of how many times a function has been called without using global variables?
This utilizes the power of Closures. A closure occurs when a nested function "remembers" the local variables of its outer function even after the outer function has finished executing.
The 'nonlocal' Keyword:
Inside the wrapper, if you try to do count += 1, Python will think count is a new local variable. By declaring nonlocal count, you tell Python to use the variable from the decorator's scope.
When you decorate an instance method, the first argument passed to the wrapper is always the instance of the class (self).
Why it works:
Since wrapper is defined with *args, when you call obj.do_work(10), args becomes (obj, 10). When the wrapper calls func(*args), it is effectively calling do_work(obj, 10), which is exactly how Python methods work internally!
Tricky Nuance: If you wrote def wrapper(x): instead of *args, the code would crash because it wouldn't know where to put the self argument.
Which of the following is the exact internal equivalent of the "Syntactic Sugar" @decorator?
It is crucial to remember that the @ symbol is just a shortcut. Understanding the manual assignment helps when you need to decorate functions dynamically at runtime.
# The @ symbol does this at the time of definition:
def my_func(): pass
my_func = decorator(my_func)
This means the name my_func no longer points to the original code you wrote; it now points to the wrapper object returned by the decorator.
How can you design a decorator that can be used both with AND without parentheses? (e.g., @log and @log(level="DEBUG"))
This is a "pro-level" pattern found in many famous libraries like pytest or click.
The Logic:
When used as @log, the first argument is the function itself.
When used as @log(level="DEBUG"), the first argument is the string "DEBUG".
By checking if callable(arg):, your decorator can decide whether to act as a decorator immediately or to return a new decorator that captures the settings.
What happens when you apply a decorator to a Class instead of a Function?
@singleton
class Database:
pass
Class decorators are highly effective for Meta-programming.
Use Cases:
Singleton Pattern: Ensure only one instance of a class is ever created.
Attribute Injection: Automatically add new methods or variables to the class.
Registration: Add the class to a global registry for a plugin system.
Just like function decorators, a class decorator takes the class, modifies it (or wraps it), and returns it.
Quick Recap of Python Decorators Concepts
If you are not clear on the concepts of Decorators, you can quickly review them here before practicing the exercises. This recap highlights the essential points and logic to help you solve problems confidently.
Python Decorators — Definition, Mechanics, and Usage
A Decorator is a design pattern that allows you to "wrap" a function or a class to extend its behavior without permanently modifying its source code. In Python, this is made possible because functions are first-class objects, meaning they can be passed as arguments, nested inside other functions, and returned just like variables.
Think of a decorator as an "envelope." The original function is the letter inside. The envelope (decorator) can have its own data and behavior (like postage stamps or security seals) that are processed before the letter itself is ever read.
Why Use Decorators — Key Benefits
Decorators allow developers to write cleaner, more modular code by separating "meta-programming" tasks from the actual logic of the application. This ensures that your core functions remain focused on their primary purpose.
Benefit
Explanation
Logic Separation
Separates "administrative" tasks like logging and security from core business logic.
Code Reusability
Write a feature once (like a performance timer) and apply it to hundreds of functions with one line.
DRY Principle
"Don't Repeat Yourself"—prevents the need to copy-paste validation logic into every function definition.
Metadata Control
Using functools.wraps ensures your function maintains its identity (name, docstrings) for debugging.
By using decorators, you can add features like memoization (caching), rate limiting, or authentication to any existing function without needing to rewrite it from scratch.
1. The Foundation: Higher-Order Functions
To build a decorator, you must understand how functions can return other functions. This "closure" property allows the inner function to remember the state of the outer function even after the outer function has finished executing. This is the "engine" that makes a decorator work.
def parent(func):
def wrapper():
print("Wrapper: Action before function.")
func() # Executing the passed-in function
print("Wrapper: Action after function.")
return wrapper
@parent
def say_hello():
print("Core: Hello World!")
# Calling say_hello now actually calls the 'wrapper'
say_hello()
2. Universal Decorators with *args and **kwargs
A professional decorator must be able to handle any function, regardless of its signature. If you do not use *args and **kwargs, your decorator will only work on functions with a specific number of arguments. By using these placeholders, you ensure your decorator is "universal."
import functools
def debug(func):
# functools.wraps preserves the original function's name and metadata
@functools.wraps(func)
def wrapper(*args, **kwargs):
print(f"Executing {func.__name__} with args:{args} kwargs:{kwargs}")
result = func(*args, **kwargs)
print(f"{func.__name__} finished. Result: {result}")
return result
return wrapper
@debug
def calculate_tax(amount, rate, discount=0):
return (amount * rate) - discount
# This works for any combination of positional and keyword arguments
calculate_tax(100, 0.15, discount=5)
3. Practical Case: Performance Benchmarking
One of the most common uses for decorators in production is measuring execution time. This allows you to identify "bottlenecks" in your application without adding time.perf_counter() logic inside every single function you want to test.
import time
from functools import wraps
def timer(func):
@wraps(func)
def wrapper(*args, **kwargs):
start_time = time.perf_counter()
# Capture the actual return value of the function
value = func(*args, **kwargs)
end_time = time.perf_counter()
run_time = end_time - start_time
print(f"Performance: {func.__name__!r} took {run_time:.4f} secs")
# Return the value so the rest of the app doesn't break
return value
return wrapper
@timer
def heavy_computation():
return sum([i**2 for i in range(1000000)])
result = heavy_computation()
4. Advanced: Decorators with Arguments (The Factory Pattern)
Sometimes the decorator itself needs configuration—such as a "repeat" decorator that needs to know how many times to execute. This requires a Decorator Factory: a function that accepts arguments and returns the actual decorator.
def repeat(num_times):
def decorator_repeat(func):
@wraps(func)
def wrapper(*args, **kwargs):
result = None
for _ in range(num_times):
# We call the function multiple times
result = func(*args, **kwargs)
return result
return wrapper
return decorator_repeat
@repeat(num_times=3)
def greet(name):
print(f"Hello {name}")
# This will print the greeting 3 times because of the argument passed to @repeat
greet("Alice")
Best Practices With Python Decorators
Always use @functools.wraps: Without this, your decorated functions will lose their original identity (like __name__ and __doc__), making it very difficult to debug.
Preserve Return Values: Always ensure your wrapper captures result = func(*args, **kwargs) and returns it at the end.
Keep Decorators Focused: A decorator should do one thing (e.g., just logging or just timing). If you need both, stack them!
Order of Execution: When stacking decorators (e.g., @auth followed by @log), the top-most decorator is the "outermost" layer. It executes first and finishes last.
Summary: Key Points
Decorators allow you to modify or enhance the behavior of a function without changing its source code.
They leverage Python's First-Class Functions, allowing logic to be passed as arguments and returned as results.
The @ symbol is syntactic sugar that makes the wrapping process readable and clean.
*args and **kwargs are essential for creating "universal" decorators that work with any function signature.
functools.wraps is a professional requirement to ensure function metadata (names and docstrings) is preserved.
Chaining allows you to layer multiple functionalities, such as authentication, logging, and performance tracking, on a single target.
Test Your Python Decorators Knowledge
Practicing Python Decorators? Don’t forget to test yourself later in our Python Quiz.
About This Exercise: Python – Decorators
At Solviyo, we see Python decorators as one of those features that truly change how you write and structure code. A decorator allows you to enhance or modify the behavior of a function without touching its original implementation. Behind the familiar @ syntax lies a powerful concept built on first-class functions and closures. In this exercise section, we focus on helping you understand that concept clearly, step by step, through practical Python exercises and well-explained MCQs.
These Python decorator exercises are designed not just to show how decorators work, but why they exist and when they should be used. You’ll move from simply applying decorators to confidently building your own. Along the way, we explain how functions are passed around, wrapped, and executed, so the “magic” becomes predictable and easy to reason about.
What You Will Learn
By working through this section, you will develop a solid, practical understanding of decorators in Python. The exercises with answers will help you explore:
First-Class Functions: How Python treats functions as objects that can be passed and returned.
Closures: Why a decorator can remember the function it wraps and any associated state.
The @ Decorator Syntax: What really happens when a decorator is applied to a function.
Decorators with Arguments: Using *args and **kwargs to write flexible, reusable decorators.
Preserving Metadata: Correctly using functools.wraps to keep function names and docstrings intact.
Why Python Decorators Matter
Decorators are essential for writing clean, maintainable Python code. They help enforce separation of concerns by keeping cross-cutting logic—such as logging, caching, authentication, or timing—out of your core business logic. Instead of repeating the same code across multiple functions, you write it once and reuse it everywhere.
In real-world projects, especially frameworks and large applications, decorators make code easier to maintain, test, and scale. Mastering this topic is a clear step toward writing professional-grade Python software.
Start Practicing
Each Solviyo Python decorator exercise includes clear explanations and answers so you can confidently apply what you learn. If you want a refresher on functions and scope, review the quick recap before starting. Then dive in and strengthen your understanding of Python decorators through hands-on practice.
Need a Quick Refresher?
Jump back to the Python Cheat Sheet to review concepts before solving more challenges.