Search code examples
pythondecorator

Is it possible to create "parameter decorators" (like TypeScript) in Python?


TypeScript has a feature known as parameter decorators — literally a decorator that you can apply to a parameter of a function or method:

class BugReport {
  // ... 
  print(@required verbose: boolean) {
    // ...
  }
}

Another example from NestJS:

class SomeController {
  // ...
  async findOne(@User() user: UserEntity) {
    // ...
  }
}

Note in the above examples that the decorators are decorating parameters of each method, not the methods themselves.

I don't think that parameter decorators like this exist in Python (at least as of v3.11, nor could I find any open PEPs that cover it); however, I'm curious to know if there is a way to implement something like this in Python?

It doesn't have to have the exact same syntax, of course; just the same effect.

I'm not super familiar with how parameter decorators work under-the-hood, but my best understanding is that they attach metadata to the corresponding function or method at compile time (so they would likely need to work in tandem with a function/method decorator, metaclass, etc.).


Solution

  • Python doesn’t have parameter decorators – only function decorators – but it does provide runtime access to type annotations, as well as typing.Annotated.

    from typing import Annotated
    
    
    class BugReport:
    
        @validate_required
        def print(self, verbose: Annotated[bool, Required]) -> None:
            ...
    
    import functools
    import inspect
    import typing
    
    
    class Required:
        pass
    
    Required = Required()
    
    
    def validate_required(fn):
        required = {
            name
            for name, t in inspect.get_annotations(fn).items()
            if typing.get_origin(t) is typing.Annotated
            and Required in typing.get_args(t)[1:]
        }
        signature = inspect.signature(fn)
    
        @functools.wraps(fn)
        def validated(*args, **kwargs):
            bound_args = signature.bind(*args, **kwargs)
    
            for required_arg in required:
                if bound_args.arguments[required_arg] is None:
                    raise ValueError(f"required argument is None: {required_arg!r}")
    
            return fn(*args, **kwargs)
    
        return validated
    

    To do this particular kind of thing in practice, you might want to extend Typeguard instead, although I honestly wouldn’t recommend it; it’s too fragile and slow.