Skip to content

Optional arguments mixed with **kwargs #11084

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
bluefish6 opened this issue Sep 9, 2021 · 6 comments
Closed

Optional arguments mixed with **kwargs #11084

bluefish6 opened this issue Sep 9, 2021 · 6 comments
Labels
bug mypy got something wrong

Comments

@bluefish6
Copy link

bluefish6 commented Sep 9, 2021

Bug Report

Mypy seems to try to compare provided arguments from **kwargs dict against specified arguments with default values.

While the code executes and runs perfectly fine in cpython 3.8.10, mypy says it's incorrect.

I think it might be related to #9676 as the initial symptom seems to match what I am observing in my case. However, I think that examples discussed there are different from the ones I encounter (as in: "possibly of different origin").

To Reproduce

somefile.py

def my_function(
    foo: bool = False,
    baz: bool = False,
    **additional,
):
    print(foo)
    print(baz)
    print(additional)


additional_kwargs = {"somearg1": 1}
my_function(**additional_kwargs)                      # line 12
my_function(foo=True, **additional_kwargs)            # line 13
my_function(baz=True, **additional_kwargs)            # line 14
my_function(foo=True, baz=True, **additional_kwargs)  # line 15

 python somefile.py 

False
False
{'somearg1': 1}

True
False
{'somearg1': 1}

False
True
{'somearg1': 1}

True
True
{'somearg1': 1}

somefile2.py

def my_function2(
    foo: bool = False,
    baz: int = 666,
    **additional,
):
    print(foo)
    print(baz)
    print(additional)


additional_kwargs = {"somearg1": 1}
my_function2(**additional_kwargs)                      # line 12
my_function2(foo=True, **additional_kwargs)            # line 13
my_function2(baz=234, **additional_kwargs)             # line 14
my_function2(foo=True, baz=234, **additional_kwargs)   # line 15
python somefile2.py 

False
666
{'somearg1': 1}

True
666
{'somearg1': 1}

False
234
{'somearg1': 1}

True
234
{'somearg1': 1}

Expected Behavior

No errors

Actual Behavior

$ mypy somefile.py 
somefile.py:12: error: Argument 1 to "my_function" has incompatible type "**Dict[str, int]"; expected "bool"
somefile.py:13: error: Argument 2 to "my_function" has incompatible type "**Dict[str, int]"; expected "bool"
somefile.py:14: error: Argument 2 to "my_function" has incompatible type "**Dict[str, int]"; expected "bool"
Found 3 errors in 1 file (checked 1 source file)

For the second file, note that no error was reported on line 13 below. My guess is that the somearg1 (int) was (incorrectly!) matched to baz (int):

mypy somefile2.py 
somefile2.py:12: error: Argument 1 to "my_function2" has incompatible type "**Dict[str, int]"; expected "bool"
somefile2.py:14: error: Argument 2 to "my_function2" has incompatible type "**Dict[str, int]"; expected "bool"
Found 2 errors in 1 file (checked 1 source file)

Your Environment

  • Mypy version used: mypy 0.910 (same results also on mypy 0.812 and even on 0.620)
  • Mypy command-line flags: none
  • Mypy configuration options from mypy.ini (and other config files): none
  • Python version used: cpython 3.8.10
  • Operating system and version: Ubuntu 20.04.1
@bluefish6 bluefish6 added the bug mypy got something wrong label Sep 9, 2021
@TRManderson
Copy link
Contributor

Another minimal example I just threw together when going to report this bug myself

def example(name: str, arg2: bool, **kwargs):
    pass

kwargs = {
    'arg2': True,
    'num': 1
}

example("test", **kwargs)

gets

$ mypy type_example.py 
type_example.py:9: error: Argument 2 to "example" has incompatible type "**Dict[str, int]"; expected "bool"

@JelleZijlstra
Copy link
Member

This is because mypy infers kwargs to be of type dict[str, int]. To get a more precise type, you need to define a TypedDict type and annotate it with that type.

@povesma
Copy link

povesma commented Oct 21, 2021

Another example, with requests:

import requests

k = {"headers": {"Cookies": ""}}
requests.request(method="get", url="https://google.com/", **k)

Output:

example.py:4: error: Argument 3 to "request" has incompatible type "**Dict[str, Dict[str, str]]"; 
expected "bool"

@pranavrajpal
Copy link
Contributor

One solution for this would be having mypy infer a more specific TypedDict-based type when a variable is marked as Final and has a dict literal assigned to it. If a dict is marked as Final, we shouldn't need to worry about false positives from code trying to set other keys, and trying to access nonexistent keys in a final dict is a genuine error, so I think making this change would be fine.

Marking the dict literals as Final in the above examples does seem to be possible (none of the dicts are being dynamically computed), and the main use case for this seems to be moving function arguments into a separate dict for readability reasons, so I'm guessing that using Final would work for most code that wanted something like this to type check.

@ShaneHarvey
Copy link

Is this a duplicate of #8862?

@JelleZijlstra
Copy link
Member

Yes

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug mypy got something wrong
Projects
None yet
Development

No branches or pull requests

6 participants