Skip to main content
🎀
PEP 614 · Speedrun · Python 3.9 (2020)

PEP 614 in 90 seconds — any expression after @ now decorates

Subscripts, attribute chains, conditionals — all legal after @.

Pre-3.9, the grammar after `@` allowed only `name` (`@cache`), `name.attr` (`@app.route`), or `name(...)` (`@retry(3)`). Anything fancier — subscript, conditional, lambda — was a `SyntaxError`: ```python DECORATORS = [cache, retry, log_args] @DECORATORS[0] # SyntaxError pre-3.9 def handler(req): ... ``` PEP 614 (Python 3.9, 2020) lifted that to **any expression** that evaluates to something callable. The grammar matches whatever you'd write in a regular call site.

What's now legal: - `@DECORATORS[0]` — subscript - `@decorators_by_env["prod"]` — dict lookup - `@(retry if is_flaky else passthrough)` — conditional - `@a.b.c.d.e` — already worked, now without the implicit-call ambiguity What the rule isn't: you STILL need the result to be callable with `(func)`. Putting `@42` after a function gets you `TypeError: 'int' object is not callable` at definition time. PEP 614 is about parser flexibility, not runtime magic.

Before — Python ≤ 3.8, restricted decorator grammar
def cache(fn):
    seen = {}
    def inner(*a):
        if a not in seen:
            seen[a] = fn(*a)
        return seen[a]
    return inner

# Pre-3.9, this was a SyntaxError:
#
# DECORATORS = [cache, ...]
# @DECORATORS[0]
# def slow_fn(x): ...
#
# Workaround: bind to a name first
my_cache = cache
@my_cache
def slow_fn(x):
    return x * 2
After — Python 3.9+, any expression after @
def add_one(fn):
    def inner(x):
        return fn(x) + 1
    return inner

def add_ten(fn):
    def inner(x):
        return fn(x) + 10
    return inner

DECORATORS = [add_one, add_ten]

# Subscript is legal — picks from a list
@DECORATORS[0]
@DECORATORS[1]
def number(x):
    return x

# Conditional / attribute chains / lambda calls also legal:
# @(retry if FLAKY else passthrough)
# @router.get("/users/{uid}")  (already worked, now without grammar quirks)

🎯 Predict the output

What does this print? Decorators apply BOTTOM-UP — the closest one wraps first.

def add_one(fn):
    def inner(x):
        return fn(x) + 1
    return inner

def add_ten(fn):
    def inner(x):
        return fn(x) + 10
    return inner

DECORATORS = [add_one, add_ten]

@DECORATORS[0]
@DECORATORS[1]
def number(x):
    return x

print(number(5))
Decorators deep dive → Senior track

Or speedrun another PEP

PEP 572Assignment as an expression — 3 lines become 1.
PEP 572 in 90 seconds — the walrus that ate your for-loop
PEP 484Python stays dynamic — but you can opt into typing.
PEP 484 in 90 seconds — type hints, without TypeScript-envy
PEP 622Switch statements are a 5% feature. Pattern matching is the 95%.
PEP 622 in 90 seconds — match / case isn't just a switch