Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Generate TypedDict from function's keyword arguments

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?

like image 432
Mario Ishac Avatar asked Sep 15 '20 22:09

Mario Ishac


People also ask

How do you pass keyword arguments?

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 ( ** ).

Does Python enforce type hints?

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 .

What is TypeVar?

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.

Does Python make hinting faster?

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.

How to use keyword arguments in Python functions?

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.

What is typeddict in Python?

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.

How to add methods to typeddict types?

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.

How to type-hint nested objects in Python with typeddict?

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.


1 Answers

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 typings 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.

like image 177
Aviv Yaniv Avatar answered Nov 02 '22 10:11

Aviv Yaniv