Skip to lesson
Python for JS Developers

FunctionsLesson 3 of 3

Decorators

3 min

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

JavaScriptPythonNote
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.namefn.__name__Dunder attribute
@decorator (TS / TC39 proposal)@decoratorNative in Python, works on plain functions
No built-in equivalent@functools.wraps(fn)Keeps the original name and docstring

Side by side

JavaScript
// 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
# 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 browser

Write a decorator called @retry:

JavaScript to convert
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