Skip to content

Instantly share code, notes, and snippets.

@oliverlambson
Last active September 15, 2024 23:26
Show Gist options
  • Save oliverlambson/1f9e06699b177fe85be6eb28f042ebfc to your computer and use it in GitHub Desktop.
Save oliverlambson/1f9e06699b177fe85be6eb28f042ebfc to your computer and use it in GitHub Desktop.
The right way to type python decorators

Fully-typed python decorators

The right way

from collections.abc import Callable
from functools import wraps


def my_decorator[T, **P](fn: Callable[P, T]) -> Callable[P, T]:
    # ... your logic
    @wraps(fn)
    def wrapped(*args: P.args, **kwargs: P.kwargs) -> T:
        # ... more logic
        return fn(*args, **kwargs)

    return wrapped
    
@my_decorator
def foo(x: int, y: str) -> bool: ...

reveal_type(foo)  # Type of "foo" is "(x: int, y: str) -> bool"

# which means it does what we expect:
foo(1, 'hi')  # ok
foo('oops')  # type error

Note the python 3.12 generics [T, **P] instead of needing T = TypeVar("T") and P = ParamSpec("P")

if you already knew about this you probably don't need to be here...

The wrong way

Often people use ... instead of a ParamSpec, which means you lose the parameter type hinting:

def my_decorator[T](fn: Callable[..., T]) -> Callable[..., T]:
    # ... your logic
    @wraps(fn)
    def wrapped(*args: Any, **kwargs: Any) -> T:  # ❌ now we lost our types
        # ... more logic
        return fn(*args, **kwargs)

    return wrapped

# use it
@my_decorator
def foo(x: int, y: str) -> bool: ...

reveal_type(foo)  # Type of "foo" is "(...) -> bool"

# which means we have no idea what it's params are:
foo(1, 'hi')  # ok
foo('oops')  # also ok, but it shouldn't be!
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment