Decorators are one of Python's most celebrated and signature design patterns. They allow you to modify or extend the behavior of a function or method without permanently altering its underlying source code .
From web frameworks like FastAPI and Flask (@app.get("/"), @login_required) to testing suites (@pytest.mark.parametrize), decorators are ubiquitous across modern Python. In this lesson, you will master decorator architecture from first principles, understand the @ syntax, preserve metadata with functools.wraps, and chain multiple decorators.
Imagine giving a box of Kaju Katli sweets to a colleague for Diwali:
Visual Architecture Blueprint Copy Diagram +-------------------------------------------------------------------------+
| THE FESTIVE GIFT WRAPPING ANALOGY |
+-------------------------------------------------------------------------+
| |
| 1. Original Core Function: |
| ──> Pure box of delicious Kaju Katli sweets |
| |
| 2. Decorator Wrapper: |
| ──> Wraps the box in sparkling golden paper |
| ──> Attaches a greeting ribbon and barcode |
| ──> Inspects freshness before opening, seals safely afterward |
| |
| 3. The Experience: |
| ──> The recipient still enjoys the original sweets, but with extra |
| security, beauty, and metadata attached! |
| |
+-------------------------------------------------------------------------+ Alternatively, think of a VIP Security Escort : The dignitary's car (original_function) drives the same road, but the police escort vehicle clears traffic before arrival and logs security confirmation after departure.
The @decorator syntax is nothing more than elegant syntactic sugar .
1 @my_decorator
2 def greet ( ) :
3 print ( "Hello!" )
4
5 # The interpreter translates the above @ syntax into this exact line:
6 greet = my_decorator ( greet )
A decorator is simply a callable that takes a function as its input, wraps it inside an inner function, and returns the modified wrapper function.
Standard Decorator Architecture Here is the canonical gold-standard blueprint for any Python decorator:
1 from functools import wraps
2
3 def my_decorator ( target_func ) :
4 """Outer function receives the target function to decorate."""
5 @wraps ( target_func ) # Crucial: preserves __name__ and __doc__!
6 def wrapper ( * args , ** kwargs ) :
7 # 1. PRE-EXECUTION LOGIC (security check, timer start, logging)
8 print ( f"[PRE] Preparing to run { target_func . __name__ } ..." )
9
10 # 2. INVOKE ORIGINAL FUNCTION
11 result = target_func ( * args , ** kwargs )
12
13 # 3. POST-EXECUTION LOGIC (timer end, cleanup, audit)
14 print ( f"[POST] Finished running { target_func . __name__ } successfully." )
15
16 # 4. RETURN RESULT
17 return result
18
19 return wrapper # Return the wrapper closure
When you wrap a function, the wrapper takes its place. Without @wraps(target_func):
func.__name__ becomes "wrapper" instead of "greet".func.__doc__ is erased or replaced by the wrapper's docstring.Debuggers, stack traces, and IDE autocompletion show confusing names! @wraps(target_func) automatically copies over the original name, docstring, parameter annotations, and module metadata.
1. Building an Execution Timer Decorator Benchmarking code performance is a classic use case for decorators:
1 import time
2 from functools import wraps
3
4 def benchmark_timer ( func ) :
5 """Measures and logs elapsed execution time of any function."""
6 @wraps ( func )
7 def wrapper ( * args , ** kwargs ) :
8 start_time = time . perf_counter ( )
9 result = func ( * args , ** kwargs )
10 end_time = time . perf_counter ( )
11 elapsed = end_time - start_time
12 print ( f"[PERF] ' { func . __name__ } ' executed in { elapsed * 1000 : .3f } ms" )
13 return result
14 return wrapper
15
16 @benchmark_timer
17 def compute_prime_sum ( limit ) :
18 """Calculates sum of all numbers up to limit."""
19 return sum ( x for x in range ( limit ) if x % 2 != 0 )
20
21 # Run decorated function
22 total = compute_prime_sum ( 1000000 )
23 print ( f"Total calculated: { total } " )
24
25 # Verify that metadata was preserved by @wraps
26 print ( "Function Name :" , compute_prime_sum . __name__ )
27 print ( "Docstring :" , compute_prime_sum . __doc__ )
Expected Output:
1 [PERF] 'compute_prime_sum' executed in 45.120 ms
2 Total calculated: 250000000000
3 Function Name : compute_prime_sum
4 Docstring : Calculates sum of all numbers up to limit.
2. Authorization Role Checker (Security Decorator) In web applications, decorators verify that a user possesses necessary administrative credentials before granting access:
1 from functools import wraps
2
3 # Simulated session state
4 CURRENT_USER = { "username" : "vikram_admin" , "role" : "ADMIN" }
5
6 def require_admin ( func ) :
7 """Guards function execution by enforcing ADMIN role authorization."""
8 @wraps ( func )
9 def wrapper ( * args , ** kwargs ) :
10 if CURRENT_USER . get ( "role" ) != "ADMIN" :
11 print ( f"[SECURITY 403] Access Denied! User ' { CURRENT_USER . get ( 'username' ) } ' is not an ADMIN." )
12 return None
13 return func ( * args , ** kwargs )
14 return wrapper
15
16 @require_admin
17 def delete_database_record ( record_id ) :
18 print ( f"[SUCCESS] Database Record # { record_id } purged permanently." )
19 return True
20
21 # Admin user executes command
22 delete_database_record ( 4092 )
23
24 # Demote user to guest and attempt execution
25 CURRENT_USER [ "role" ] = "GUEST"
26 delete_database_record ( 4092 )
Expected Output:
1 [SUCCESS] Database Record #4092 purged permanently.
2 [SECURITY 403] Access Denied! User 'vikram_admin' is not an ADMIN.
3. Chaining Multiple Decorators: The Onion Peeling Order When multiple decorators are applied to a single function, they execute in a specific order:
1 from functools import wraps
2
3 def bold ( func ) :
4 @wraps ( func )
5 def wrapper ( * args , ** kwargs ) :
6 return f"<b> { func ( * args , ** kwargs ) } </b>"
7 return wrapper
8
9 def italic ( func ) :
10 @wraps ( func )
11 def wrapper ( * args , ** kwargs ) :
12 return f"<i> { func ( * args , ** kwargs ) } </i>"
13 return wrapper
14
15 @bold
16 @italic
17 def format_announcement ( msg ) :
18 return msg
19
20 # Evaluated as: bold(italic(format_announcement))
21 # 'italic' wraps first, then 'bold' wraps outside!
22 print ( format_announcement ( "Diwali Festive Sale!" ) )
Expected Output:
1 <b><i>Diwali Festive Sale!</i></b>
Note [!NOTE]
Chaining Order Rule:
Decorators stack bottom-up at definition time (italic wraps first, then bold), but execute top-down at runtime (bold's wrapper runs before italic's wrapper).
Best Practices & Comparison: Do's and Don'ts Practice Bad / Error-Prone Pattern Recommended Gold Standard Metadata Preservation Writing wrappers without @wraps Always apply @wraps(func) to inner wrapper Parameter Handling Rigid parameters: def wrapper(a, b): Universal forwarding: def wrapper(*args, **kwargs): Return Values Forgetting to return result from wrapper Always capture and return the original function's result Side-Effects Executing code at decoration time instead of call time Put runtime logic inside wrapper, not outer decorator Complex Nesting 5 chained decorators on 1 function Avoid excessive decorator stacking; combine logic if possible
Definition: A callable that takes a function, extends its behavior via a closure wrapper, and returns the modified callable.Syntactic Sugar: @dec directly translates to fn = dec(fn).*` args, kwargs`: Enables the wrapper to accept any arbitrary positional and keyword arguments. @functools.wraps: Essential helper that preserves the original function's identity, docstring, and annotations.Chaining: Stacks bottom-to-top at decoration time, runs top-to-bottom at execution time.Practice Challenge Scenario: Indian Retail Banking Transaction Audit Trail Decorator Build an enterprise audit decorator @audit_transaction for an online banking portal:
1 Captures the account ID from the first positional argument.
2 Formats a log message: "[AUDIT START] <function_name> invoked for Account: <account_id>".
3 Invokes the original transaction function.
4 If an exception occurs, logs "[AUDIT FAILED] <error_type>: <error_message>" and re-raises the exception.
5 If successful, logs "[AUDIT SUCCESS] Transaction completed successfully".
6 Uses @wraps to preserve function identity.
Starter Code 1 from functools import wraps
2
3 def audit_transaction ( func ) :
4 # TODO: Implement audit wrapper with error handling and metadata preservation
5 pass
Complete Solution 1 from functools import wraps
2
3 def audit_transaction ( func ) :
4 @wraps ( func )
5 def wrapper ( account_id , * args , ** kwargs ) :
6 print ( f"[AUDIT START] ' { func . __name__ } ' initiated for Account: { account_id } " )
7 try :
8 result = func ( account_id , * args , ** kwargs )
9 print ( f"[AUDIT SUCCESS] Transaction completed successfully." )
10 return result
11 except Exception as err :
12 print ( f"[AUDIT FAILED] { type ( err ) . __name__ } : { err } " )
13 raise err
14 return wrapper
15
16 @audit_transaction
17 def withdraw_cash ( account_id , amount ) :
18 """Processes cash withdrawal with minimum balance safety."""
19 current_balance = 10000.0 # Simulated balance
20 if amount > current_balance :
21 raise ValueError ( f"Insufficient funds! Requested ₹ { amount : , } but balance is ₹ { current_balance : , } " )
22 return f"Dispensed ₹ { amount : , } . Remaining: ₹ { current_balance - amount : , } "
23
24 # Test 1: Successful Transaction
25 print ( "--- Test 1: Valid Withdrawal ---" )
26 tx1 = withdraw_cash ( "SBIN-9901" , 4000.0 )
27 print ( "Result:" , tx1 )
28
29 # Test 2: Failed Transaction (Catches and logs failure)
30 print ( "\n--- Test 2: Insufficient Funds ---" )
31 try :
32 withdraw_cash ( "SBIN-9901" , 15000.0 )
33 except ValueError :
34 print ( "[TEST PASSED] Exception was accurately intercepted and logged by audit decorator." )
Expected Output 1 --- Test 1: Valid Withdrawal ---
2 [AUDIT START] 'withdraw_cash' initiated for Account: SBIN-9901
3 [AUDIT SUCCESS] Transaction completed successfully.
4 Result: Dispensed ₹4,000.0. Remaining: ₹6,000.0
5
6 --- Test 2: Insufficient Funds ---
7 [AUDIT START] 'withdraw_cash' initiated for Account: SBIN-9901
8 [AUDIT FAILED] ValueError: Insufficient funds! Requested ₹15,000.0 but balance is ₹10,000.0
9 [TEST PASSED] Exception was accurately intercepted and logged by audit decorator.
Interactive Quiz • Knowledge Check
Practice Quiz What is the expression @my_decorator above def my_func(): equivalent to in standard Python?
A my_func = my_decorator(my_func)
B my_decorator = my_func()
C my_func() + my_decorator()
D import my_decorator
Why should @functools.wraps(func) be applied to the inner wrapper function?
A It compiles the function to C code
B It preserves the original function's metadata such as __name__ and __doc__, preventing them from being overwritten by the wrapper
C It allows functions to run without arguments
D It prevents the function from ever raising exceptions
If a function is decorated with both @decorator_one and @decorator_two:
Python Code Snippet
1 @decorator_one
2 @decorator_two
3 def action ( ) : pass
A decorator_two(decorator_one(action))
B decorator_one(decorator_two(action))
C Randomly depending on system memory
D Simultaneously in parallel threads
What happens if a wrapper function omits return result after calling the target function?
A The target function automatically returns True
B Any caller of the decorated function will receive None instead of the target function's actual return value
C Python throws a SyntaxError
D The operating system reboots
Why do wrapper functions typically declare def wrapper(args, *kwargs):?
A Because Python prohibits any other parameter names
B To enable the decorator to wrap any function regardless of its parameter signature or arity
C To convert inputs into hexadecimal numbers
D To disable type checking
In the next lesson, we will continue your intermediate Python journey with Project: Function Decorator Example (2: Functions Deep Dive).
👉 Continue to Next Lesson: Project: Function Decorator Example →