foo.py
:
kwargs = {"a": 1, "b": "c"}
def consume(*, a: int, b: str) -> None:
pass
consume(**kwargs)
mypy foo.py
:
error: Argument 1 to "consume" has incompatible type "**Dict[str, object]"; expected "int"
error: Argument 1 to "consume" has incompatible type "**Dict[str, object]"; expected "str"
This is because object
is a supertype of int
and str
, and is therefore inferred. If I declare:
from typing import TypedDict
class KWArgs(TypedDict):
a: int
b: str
and then annotate kwargs
as KWArgs
, the mypy
check passes. This achieves type safety, but requires me to duplicate the keyword argument names and types for consume
in KWArgs
. Is there a way to generate this TypedDict
from the function signature at type checking time, such that I can minimize the duplication in maintenance?
Kwargs allow you to pass keyword arguments to a function. They are used when you are not sure of the number of keyword arguments that will be passed in the function. Kwargs can be used for unpacking dictionary key, value pairs. This is done using the double asterisk notation ( ** ).
The Python runtime does not enforce function and variable type annotations. They can be used by third party tools such as type checkers, IDEs, linters, etc. This module provides runtime support for type hints. The most fundamental support consists of the types Any , Union , Callable , TypeVar , and Generic .
In short, a TypeVar is a variable you can use in type signatures so you can refer to the same unspecified type more than once, while a NewType is used to tell the type checker that some values should be treated as their own type.
PEP 484 introduced type hints — a way to make Python feel statically typed. While type hints can help structure your projects better, they are just that — hints — and by default do not affect the runtime.
While defining a function, we set some parameters within it. We accept data in these parameters from function calls. Now, python provides the feature of accepting the parameters in several ways, e.g., positional arguments or keyword arguments. Keyword Arguments are used when we used to assign values to the function parameter as the key-value pair.
TypedDict was introduced in Python 3.8 to provide type Hints for Dictionaries with a Fixed Set of Keys. The TypedDict allows us to describe a structured dictionary/map with an expected set of named string keys mapped to values of particular expected types, which Python type-checkers like mypy can further use.
TypedDict objects are regular dictionaries at runtime, and TypedDict cannot be used with other dictionary-like or mapping-like classes, including subclasses of dict. There is no way to add methods to TypedDict types. The motivation here is simplicity. TypedDict type definitions could plausibly used to perform runtime type checking of dictionaries.
The TypedDict ABC has three items: a (type str), b (type str), and c (type int). However, a TypedDict cannot inherit from both a TypedDict type and a non-TypedDict base class. Using inheritance, we can create complex TypedDict to type-hint nested objects in Python. For these, each class would have to be a subclass of TypedDict.
To the best of my knowledge, there is no direct workaround on this [1], but there is another elegant way to achieve exactly that:
We can utilize the typing
s NamedTuple
to create an object that holds the parameter:
ConsumeContext = NamedTuple('ConsumeContext', [('a', int), ('b', str)])
Now we define the consume
method to accept it as a parameter:
def consume(*, consume_context : ConsumeContext) -> None:
print(f'a : {consume_context.a} , b : {consume_context.b}')
The whole code would be:
from typing import NamedTuple
ConsumeContext = NamedTuple('ConsumeContext', [('a', int), ('b', str)])
def consume(*, consume_context : ConsumeContext) -> None:
print(f'a : {consume_context.a} , b : {consume_context.b}')
ctx = ConsumeContext(a=1, b='sabich')
consume(consume_context=ctx)
And running mypy would yield:
Success: no issues found in 1 source file
It will recognize that a
and b
are parameters, and approve that.
And running the code would output:
a : 1 , b : sabich
However, if we change b
to be not a string, mypy will complain:
foo.py:9: error: Argument "b" to "ConsumeContext" has incompatible type "int"; expected "str"
Found 1 error in 1 file (checked 1 source file)
By this, we achieve type checking for a method by defining once it's parameters and types.
[1] Because if either defining TypedDict
or function signature, based on the other, would require to know the other's __annotations__
, which is not known on check-time, and defining a decorator to cast types on run-time misses the point of type checking.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With