Search code examples
pythonmypypython-typingpyright

Why can't you use Dict[str, Any] or Mapping[str, Any] to type **kwargs? How are you supposed to type it?


I am currently trying to learn how to best include types in my Python code, but I'm a little perplexed on how to properly type **kwargs in my function signatures.

A minimal example of what I currently have:

def foo(name: str, **kwargs: Dict[str, Any]) -> int:

    modified_name: str = f'This is an example for {name}!'

    return some_other_func(modified_name, **kwargs)


def bar(id_number: int, **kwargs: Dict[str, Any]) -> int:

    name: str = some_func(id_number)
    
    return foo(name, number=id_number, **kwargs)

What I expect to happen: Since foo does not specify the number keyword argument, I expect it to get collected into foo's kwargs, which is essentially a Dict[str, Any]. Since "int" is an "Any" type I should have no complaints about typing.

What really happens: At runtime number is collected into foo's kwargs and some_other_func can safely pull out the number keyword argument. However, mypy (version 1.1.1) complains with the following

error: Argument "number" to "foo" has incompatible type "int"; expected "Dict[str, Any]"

A similar complaint from pyright (version 1.1.292):

Argument of type "int" cannot be assigned to parameter "number" of type "Dict[str, Any]" in function "foo" "int" is incompatible with "Dict[str, Any]"

Is this expected behavior? Should I expect type checkers to complain if I am passing keyword arguments the direct callee doesn't have in the function signature?

Continuing this I thought okay, I will just add number as a key in kwargs directly (whether this is good practice I'm not sure, but this is besides the point), so this way I will certainly be passing a Dict[str, Any]. This is essentially what I tried:

def bar(id_number: int, **kwargs: Dict[str, Any]) -> int:

    name: str = some_func(id_number)
    
    kwargs[number] = id_number
    
    return foo(name, **kwargs)

But now I get the error (from mypy):

error: Incompatible types in assignment (expression has type "int", target has type "Dict[str, Any]"

Similarly from pyright:

Argument of type "int" cannot be assigned to parameter "__value" of type "Dict[str, Any]" in function "__setitem__"  "int" is incompatible with "Dict[str, Any]"

But this is even more confusing, why do they seem to imply I'm assigning to kwargs when really I'm assigning to a key in kwargs?

Does it then mean I'm supposed to be typing the values inside of kwargs then?

If instead I type kwargs as "Union[int, Dict[str, Any]]" I get no issue. So this seems to confirm the question. But why the different behavior? Is it simply convenience that we should know kwargs is a mapping, so avoid the extra typing wrap? But I don't see this stated anywhere and I'm not 100% convinced I am correct on how this works.

Finally, I've also tried using Mapping[str, Any] intead of Dict[str, Any], but it has same issue.

tl;dr: kwargs is a dictionary containing keywords not specified by the function signature and it can be accessed and modified like a Dict[str, Any]. Why can't I type it like one? How should I really type it?


Solution

  • When you using **kwargs, the key is always going to be a string. For type-checking the value of that key, you only need to type check one possible value for the key. For more detail, check out this question and it's answer.