{
  "$type": "site.standard.document",
  "canonicalUrl": "https://rednafi.com/python/static-typing-decorators/",
  "description": "Type Python decorators accurately using ParamSpec and Concatenate to preserve wrapped function signatures and enable proper static analysis.",
  "path": "/python/static-typing-decorators/",
  "publishedAt": "2022-01-23T00:00:00.000Z",
  "site": "at://did:plc:fgtm2c26vfcj74rfmeggbyqj/site.standard.publication/3mnl6f7ob462z",
  "tags": [
    "Python",
    "Typing"
  ],
  "textContent": "Accurately static typing decorators in Python is an icky business. The wrapper function\nobfuscates type information required to statically determine the types of the parameters and\nthe return values of the wrapped function.\n\nLet's write a decorator that registers the decorated functions in a global dictionary during\nfunction definition time. Here's how I used to annotate it:\n\nThe functools.wraps decorator makes sure that the identity and the docstring of the\nwrapped function don't get gobbled up by the decorator. This is syntactically correct and if\nyou run Mypy against the code snippet, it'll happily tell you that everything's alright.\nHowever, this doesn't exactly do anything. If you call the hello function with the wrong\ntype of parameter, Mypy won't be able to detect the mistake statically. Notice this:\n\nAll this for nothing!\n\n[PEP-612] proposed ParamSpec and Concatenate in the typing module to address this\nissue. Later on, these were introduced in Python 3.10. The former is required to precisely\nadd type hints to any decorator while the latter is needed to type annotate decorators that\nchange wrapped functions' signatures.\n\n> If you're not on Python 3.10+, you can import ParamSpec and Concatenate from the\n> typing_extensions module. The package gets automatically installed with Mypy.\n\nUse ParamSpec to type decorators\n\nI'll take advantage of both ParamSpec and TypeVar to annotate the register decorator\nthat we've seen earlier:\n\nAbove, I've used ParamSpec to annotate the type of the wrapped function's input parameters\nand TypeVar to annotate its return value. Underneath, ParamSpec is a type variable\nsimilar to TypeVar but with a trick under its sleeve; it can relay type information to a\ndecorator's inner callable.\n\nNotice the annotations of the inner function inside register. Here, P.args and\nP.kwargs are transferring the type information from the wrapped func to the inner\nfunction. This makes sure that static type checkers like Mypy can now precisely scream at\nyou whenever you call the decorated functions with the wrong type of parameters.\n\nUse Concatenate to type decorators that change the wrapped functions' signatures\n\nThere's another type of decorator that changes the signature of the wrapped function by\nadding or removing parameters during runtime. Annotating these can be tricky; as the magic\nhappens mostly during runtime. The Concatenate type allows us to communicate this behavior\nwith the type checker.\n\nConsider this inject_logger decorator, that adds a logger instance to the decorated\nfunction. It sort of acts how Django injects the request instances into the view*\nfunctions. Here's the typed version of that:\n\nThis is a contrived example and a gratuitously complicated way to achieve a simple goal.\nAlso, it's not recommended to mutate function signatures like this in runtime. But it's\nallowed and now Python gives you a way to statically type check the decorator and the\ndecorated function.\n\nThe only thing that's different from the previous section is the annotation of the func\nparameter of the inject_logger. Notice how the Callable generic now contain\nConcatenate[logging.Logger, P]. The first parameter of the Concatenate generic is the\ninjected parameter - logging.Logger in this case. Since the instance of logging.Logger\ngets dynamically injected, an additional paradigm Concatenate is necessary to communicate\nthat with the type checker.\n\nIf you'd defined hello with the wrong types, the type checker would've complained.\n\nAbove, I've changed the type of the logger parameter from logging.Logger to int. The\ntype checker will now dutifully chastise us for our transgressions.\n\nUnfortunately, as of writing this post, Mypy doesn't understand Concatenate but\nMicrosoft's [Pyright] does. You can pip install Pyright and test out the above snippet as\nfollows:\n\nThis will return:\n\nFurther reading\n\n- [Decorator typing (PEP 612) - Anthony explains #386]\n\n\n\n\n[pep-612]:\n    https://www.python.org/dev/peps/pep-0612/\n\n[pyright]:\n    https://github.com/microsoft/pyright\n\n[decorator typing (pep 612) - anthony explains #386]:\n    https://www.youtube.com/watch?v=fwZoxWyMGM8",
  "title": "Static typing Python decorators"
}