FunctionsLesson 3 of 3
Decorators
You already know higher-order functions from JS: a function that takes a function and returns a modified version. Decorators are exactly that, with syntactic sugar. Instead of writing loggedAdd = withLogging(add), you put @with_logging above the function definition.
The @ symbol is not magic. When Python sees @timer above def slow_function(), it runs slow_function = timer(slow_function). That is the entire trick. Once you realize this, decorators stop being mysterious.
You may have seen @decorators in TypeScript or Angular. In JS they are a TC39 proposal (supported by TypeScript and Babel) and only work on classes and class members. Python decorators have been part of the language since 2004 and work on any function or class.
Decorators with arguments add one more layer of nesting: the outer function takes the arguments and returns a decorator, which takes the function and returns the wrapper. Three levels deep. It looks complex but follows the same pattern every time.
One detail to get right from day one: wrap your inner function with @functools.wraps(fn). Without it, the decorated function loses its __name__ and docstring, which confuses debuggers, logs, and tools like pytest.
Quick reference
| JavaScript | Python | Note |
|---|---|---|
| const logged = withLog(fn) | @with_log above def fn(): | @ is syntax sugar for fn = with_log(fn) |
| function(...args) {} | def wrapper(*args, **kwargs): | Capture all args |
| fn.name | fn.__name__ | Dunder attribute |
| @decorator (TS / TC39 proposal) | @decorator | Native in Python, works on plain functions |
| No built-in equivalent | @functools.wraps(fn) | Keeps the original name and docstring |
Side by side
// JavaScript - HOF pattern
function withLogging(fn) {
return function(...args) {
console.log("Calling " + fn.name);
const result = fn(...args);
console.log("Result: " + result);
return result;
};
}
function add(a, b) { return a + b; }
const loggedAdd = withLogging(add);# Python - same idea, cleaner syntax
import functools
def with_logging(fn):
@functools.wraps(fn)
def wrapper(*args, **kwargs):
print(f"Calling {fn.__name__}")
result = fn(*args, **kwargs)
print(f"Result: {result}")
return result
return wrapper
@with_logging # = add = with_logging(add)
def add(a, b):
return a + b
# Decorator WITH arguments (3 levels)
def repeat(n):
def decorator(fn):
@functools.wraps(fn)
def wrapper(*args, **kwargs):
for _ in range(n):
result = fn(*args, **kwargs)
return result
return wrapper
return decorator
@repeat(3)
def say_hello():
print("Hello!")Exercise
Real Python, running in your browserWrite a decorator called @retry:
Requirements:
1. Takes a max_attempts argument
2. Retries the function when it raises an exception
3. If all attempts fail, re-raise the last exception
4. Print the attempt number on each try
5. Pass all arguments through to the function
@retry(max_attempts=3)
def risky():
...Ctrl + Enter runCtrl + Shift + Enter checkTab indentEsc then Tab to leave the editorCtrl + / comment