Q1Which of the following has the same meaning as putting @logger above def greet(): ...?
Decorators — Adding Behavior to Functions with @
Walk through how Python decorators add behavior to functions with @, from the basics through arguments — all runnable in your browser.
By the end of the previous article on lambdas, you'd seen the main ways to treat functions as values. To wrap up, let's pin down decorators — the dedicated syntax for layering extra behavior on top of a function.
What Is a Decorator?
A decorator is a way to add behavior before and after a function without changing the function itself. Things like "log the call," "time the run," or "cache the result" — shared behavior you want to layer onto lots of functions — can live in one place.
The syntax is just one line above the function definition: @decorator_name. Python reads that as "the same as func = decorator_name(func)".
@logger above def greet(): makes Python run greet = logger(greet) internally, replacing greet with a new function wrapped by logger.# The decorator itself (a higher-order function that takes and returns a function)
def logger(func):
def wrapper():
print("=== start ===")
func() # call the original function
print("=== end ===")
return wrapper
# Caller side: just add @
@logger
def greet(): # → logger(greet)
print("Hello")
greet()
# === start ===
# Hello
# === end ===
# Internally equivalent to:
# def greet():
# print("Hello")
# greet = logger(greet)
The Basic Decorator — Wrapping a Function with wrapper
The skeleton of a decorator is a 3-step shape: the outer function takes func, the inner function (conventionally wrapper) calls func(), and you return wrapper. The wrapper that keeps func available as it runs is exactly a closure.
Whatever you write before and after func() runs every time the decorated function is called.
def logger(func):
def wrapper(*args, **kwargs):
print(f"[LOG] running {func.__name__}")
result = func(*args, **kwargs)
print(f"[LOG] {func.__name__} done")
return result
return wrapper
@logger
def greet(name):
return f"Hello, {name}"
print(greet("Alice"))
# [LOG] running greet
# [LOG] greet done
# Hello, Alice
- greet is replaced by the wrapper function
- The original
greetbody lives on asfuncinsidewrapper
funcholds the originalgreet- Builds
wrapperinside and returns it
- Runs pre →
func()→ post in order - From the outside, this is the new
greet
Pass Any Arguments Through with *args / **kwargs
So far, wrapper took no arguments. When you want to decorate functions that do take arguments, use *args / **kwargs to accept any arguments as-is and forward them straight to `func`.
That turns the decorator into a generic one that works with any function signature. It handles add(2, 3) (positional) and add(2, 3, name="ABC") (keyword) with the same decorator.
def log_call(func):
def wrapper(*args, **kwargs):
print(f"call: args={args}, kwargs={kwargs}")
result = func(*args, **kwargs) # unpack and forward here too
print(f"result: {result}")
return result # don't forget to return it
return wrapper
@log_call
def add(a, b):
return a + b
print(add(2, 3))
# call: args=(2, 3), kwargs={}
# result: 5
# 5
print(add(2, b=3))
# call: args=(2,), kwargs={'b': 3}
# result: 5
# 5
Don't Forget to return the Result
If wrapper only writes result = func(...) and forgets the return, the decorated function's return value silently turns into None. The classic accident is finding add(2, 3) quietly returning None — when you write a decorator, treat return result as part of the same muscle memory.
Knowledge Check
Answer each question one by one.
Q2What does this code print?def deco(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs) * 2
return wrapper
@deco
def plus(a, b):
return a + b
print(plus(3, 4))