dylanjm
dylanjm

Reputation: 2101

MyPy type hints for a function defined with named parameters but can take **kwargs?

Apologies for the mess of a title:

Suppose I have a function defined that takes a number of parameters.

Now later in my code, I use a dictionary to pass into test() all the keyword args.

import argparse
from typing import Dict, Callable

def test(a: str, b: Dict[str, str], c: str, d: argparse.Namespace, e: Callable) -> None:
    if d.t:
        print(f"{a} to the {b['foo']} to the {c}!")
    else:
        print(f"{a} to the {b['foo']} to the {c} also, {e()}")

def hello() -> str:
    return "Hello"

if __name__ == '__main__':
    parser = argparse.ArgumentParser()
    parser.add_argument("-t", action="store_true", default=False)
    args = parser.parse_args()

    b = {'foo': 'bar'}
    info = {'b': b, "c": "foobar", "d": args, "e": hello}
    test("foo", **info)

When I run MyPy on my code, I get the following errors:

test.py:21: error: Argument 2 to "test" has incompatible type "**Dict[str, object]"; expected "Dict[str, str]"
test.py:21: error: Argument 2 to "test" has incompatible type "**Dict[str, object]"; expected "str"
test.py:21: error: Argument 2 to "test" has incompatible type "**Dict[str, object]"; expected "Namespace"
test.py:21: error: Argument 2 to "test" has incompatible type "**Dict[str, object]"; expected "Callable[..., Any]"
Found 4 errors in 1 file (checked 1 source file)

Why am I getting this? And how can I properly type hint the function to allow this?

Upvotes: 0

Views: 1935

Answers (1)

Mario Ishac
Mario Ishac

Reputation: 5877

The reason the example you had before type checks is because all values of the plot_info are str (homogeneous), and all keyword arguments being passed into the function are also str. There is no possibility for type mismatch, so mypy reports success.

In your new example, the heterogeneous types of info cannot be encoded into its type individually. Let's see what happens when we run reveal_type(info):

note: Revealed type is 'builtins.dict[builtins.str*, builtins.object*]'

So we see that mypy has to choose a homogeneous type for the value, and it goes with object. Can mypy guarantee that a dictionary of objects satisfies your individual type constraints for b, c, d, e, given that your passing objects under its eyes? Nope, so it errors.

To preserve the granular types of b, c, d, e, we can use TypedDict, which was made to support heterogeneously-typed values in Dicts:

class Info(TypedDict):
    b: Dict[str, str]
    c: str
    d: argparse.Namespace
    e: Callable

and change the info declaration to:

info: Info = {'b': b, "c": "foobar", "d": args, "e": hello}

This causes mypy to report success.

We have to duplicate the argument names and types. Sadly, Generate TypedDict from function's keyword arguments shows no direct workaround for this.

Upvotes: 3

Related Questions