Skip to content

Instantly share code, notes, and snippets.

@ngshiheng
Created August 6, 2025 13:35
Show Gist options
  • Save ngshiheng/c2e3f8009238505d19ba271de0c43b1b to your computer and use it in GitHub Desktop.
Save ngshiheng/c2e3f8009238505d19ba271de0c43b1b to your computer and use it in GitHub Desktop.
Decorators

Basic Decorator Typing

For simple decorators, use Callable:

from typing import Callable, Any
from functools import wraps

def my_decorator(func: Callable[..., Any]) -> Callable[..., Any]:
    @wraps(func)
    def wrapper(*args: Any, **kwargs: Any) -> Any:
        print(f"Calling {func.__name__}")
        return func(*args, **kwargs)
    return wrapper

@my_decorator
def greet(name: str) -> str:
    return f"Hello, {name}!"

Using TypeVar for Type Preservation

To preserve the original function's type signature:

from typing import TypeVar, Callable, Any
from functools import wraps

F = TypeVar('F', bound=Callable[..., Any])

def preserve_signature(func: F) -> F:
    @wraps(func)
    def wrapper(*args: Any, **kwargs: Any) -> Any:
        print(f"Calling {func.__name__}")
        return func(*args, **kwargs)
    return wrapper  # type: ignore

@preserve_signature
def add(x: int, y: int) -> int:
    return x + y

ParamSpec for Parameter Specification (Python 3.10+)

ParamSpec provides more precise typing for function parameters:

from typing import ParamSpec, TypeVar, Callable
from functools import wraps

P = ParamSpec('P')
T = TypeVar('T')

def log_calls(func: Callable[P, T]) -> Callable[P, T]:
    @wraps(func)
    def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
        print(f"Calling {func.__name__} with args={args}, kwargs={kwargs}")
        return func(*args, **kwargs)
    return wrapper

@log_calls
def calculate(x: int, y: int, operation: str = "add") -> int:
    return x + y if operation == "add" else x - y

Decorators with Arguments

For parameterized decorators:

from typing import ParamSpec, TypeVar, Callable, Any
from functools import wraps

P = ParamSpec('P')
T = TypeVar('T')

def retry(max_attempts: int) -> Callable[[Callable[P, T]], Callable[P, T]]:
    def decorator(func: Callable[P, T]) -> Callable[P, T]:
        @wraps(func)
        def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
            for attempt in range(max_attempts):
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    if attempt == max_attempts - 1:
                        raise e
            return func(*args, **kwargs)  # This line won't be reached
        return wrapper
    return decorator

@retry(3)
def unreliable_function() -> str:
    import random
    if random.random() < 0.7:
        raise ValueError("Random failure")
    return "Success!"

Class-based Decorators

from typing import ParamSpec, TypeVar, Callable, Any
from functools import wraps

P = ParamSpec('P')
T = TypeVar('T')

class TimingDecorator:
    def __call__(self, func: Callable[P, T]) -> Callable[P, T]:
        @wraps(func)
        def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
            import time
            start = time.time()
            result = func(*args, **kwargs)
            print(f"{func.__name__} took {time.time() - start:.4f} seconds")
            return result
        return wrapper

timer = TimingDecorator()

@timer
def slow_function() -> None:
    import time
    time.sleep(1)

Method Decorators

For decorating methods, consider the self parameter:

from typing import ParamSpec, TypeVar, Callable, Any, cast
from functools import wraps

P = ParamSpec('P')
T = TypeVar('T')

def method_decorator(func: Callable[P, T]) -> Callable[P, T]:
    @wraps(func)
    def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
        print(f"Calling method {func.__name__}")
        return func(*args, **kwargs)
    return cast(Callable[P, T], wrapper)

class MyClass:
    @method_decorator
    def my_method(self, value: int) -> str:
        return f"Value is {value}"

Generic Decorators

For more complex scenarios with generics:

from typing import TypeVar, Generic, Callable, ParamSpec
from functools import wraps

P = ParamSpec('P')
T = TypeVar('T')
U = TypeVar('U')

def transform_result(transform_func: Callable[[T], U]) -> Callable[[Callable[P, T]], Callable[P, U]]:
    def decorator(func: Callable[P, T]) -> Callable[P, U]:
        @wraps(func)
        def wrapper(*args: P.args, **kwargs: P.kwargs) -> U:
            result = func(*args, **kwargs)
            return transform_func(result)
        return wrapper
    return decorator

@transform_result(str.upper)
def get_greeting(name: str) -> str:
    return f"hello, {name}"

TL;DR

  1. Use ParamSpec for better parameter type preservation
  2. Use TypeVar with bounds for maintaining return types
  3. @wraps is essential for preserving metadata
  4. Type checkers like mypy work better with these patterns
  5. Generic decorators are fully supported with proper typing
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment