Chris
Chris

Reputation: 31186

How does one pass keyword args with mypy?

Suppose:

def foo(biz: str = 'biz',baz: str = 'baz', *args:Any, **kwargs:Any)-> str:
   return biz + baz

And I use this like:

def bar(ness: str = 'ness', *args, **kwargs):
   return foo(biz=ness, *args, **kwargs)

Note that this goal (specifying keywords and passing args + kwargs) shadowed some other misunderstanding on my part related to args: args, in python, can't be passed in after an arg with a default value (a keyword arg) has been set, as correctly pointed out below..

Then mypy version 0.812 throws an error like:

$: mypy --ignore-missing-imports --disallow-untyped-defs --disallow-incomplete-defs

foo/foo.py:6: error: "foo" gets multiple values for keyword argument "biz"

Is there a way around this problem in particular without making significant changes anywhere other than the return line below?

   return foo(biz=ness, *args, **kwargs)

Upvotes: 3

Views: 1903

Answers (3)

Chris
Chris

Reputation: 31186

This is the answer I was looking for, in the end:

def bar(baz='ness', *args, **kwargs):
   return foo(*args, **dict(kwargs, baz=baz)

>>> bar()
bizness

Upvotes: 1

Jean Hominal
Jean Hominal

Reputation: 16796

From your answer, I will assume that you want the linter to pass on the definition of bar, and that you are willing to make changes on the implementation / declaration of bar for that purpose.

For me, the root issue is that the implementation of bar is wrong - if *args is not empty, then the call foo(biz=ness, *args, **kwargs) will fail with exactly the error that is reported by mypy.

def foo(biz: str = 'biz',baz: str = 'baz', *args:Any, **kwargs:Any)-> str:
    return biz + baz

def bar_from_question(ness: str = 'ness', *args, **kwargs):
    return foo(biz=ness, *args, **kwargs)
>>> bar_from_question("1")
1baz
>>> bar_from_question("1", "2")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Users/jean/Projects/python/test-mypy/test.py", line 7, in bar_from_question
     return foo(biz=ness, *args, **kwargs)
TypeError: foo() got multiple values for argument 'biz'

For me, you have three options:

  1. Fix the call to foo by not passing args anymore. Such a change entails no loss in functionality, as having a non-empty args in bar raised a TypeError anyway. In that case, you could also remove args from the signature of bar (again, without losing functionality - you simply replace a TypeError triggered by the call to foo with a TypeError in the call to bar)

    def bar_do_not_pass_args_to_foo(ness: str = 'ness', *args, **kwargs):
        return foo(biz=ness, **kwargs)
    
    def bar_remove_args_from_signature(ness: str = 'ness', **kwargs):
        return foo(biz=ness, **kwargs)
    
  2. Fix the call to foo by passing biz as a positional argument instead of a named argument:

    def bar_pass_biz_as_positional(ness: str = 'ness', *args, **kwargs):
        return foo(ness, *args, **kwargs)
    
  3. Hiding the issue from mypy (as I understand from your answer)

    def bar_hide_from_mypy(ness: str = 'ness', *args, **kwargs):
        kwargs['biz'] = ness
        return foo(*args, **kwargs)
    

    However, if the objective is to simply prevent mypy from reporting an error, there is syntax for that:

    def bar_suppress_mypy(ness: str = 'ness', *args, **kwargs):
        return foo(biz=ness, *args, **kwargs)  # type: ignore
    

Now, if I compare the results of each proposal in a runtime interpreter:

>>> bar_do_not_pass_args_to_foo("1")
'1baz'
>>> bar_do_not_pass_args_to_foo("1", "2")
'1baz'
>>> bar_remove_args_from_signature("1")
'1baz'
>>> bar_remove_args_from_signature("1", "2")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: bar_remove_args_from_signature() takes from 0 to 1 positional arguments but 2 were given
>>> bar_pass_biz_as_positional("1")
'1baz'
>>> bar_pass_biz_as_positional("1", "2")
'12'
>>> bar_hide_from_mypy("1")
'1baz'
>>> bar_hide_from_mypy("1", "2")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Users/jean/Projects/python/test-mypy/test.py", line 20, in bar_hide_from_mypy
    return foo(*args, **kwargs)
TypeError: foo() got multiple values for argument 'biz'
>>> bar_suppress_mypy("1")
'1baz'
>>> bar_suppress_mypy("1", "2")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Users/jean/Projects/python/test-mypy/test.py", line 23, in bar_suppress_mypy
    return foo(biz=ness, *args, **kwargs)  # type: ignore
TypeError: foo() got multiple values for argument 'biz'

You can see that:

  • for every case where bar_from_question returns a result, every other bar_... implementation returns the same result;
  • every implementation except bar_from_question is silent on mypy, which I am under the impression is what you wanted;

In your situation, I would go for a solution to the underlying issue (that is, either applying bullet point 1, or 2), rather than just trying to coerce mypy (bullet point 3).

Upvotes: 2

chepner
chepner

Reputation: 530843

Despite the ordering in the call to foo, the values in args are assigned to the positional parameters before the keyword argument biz is considered. That means that that bar, which could be called with positional arguments, could have a value assigned to biz before ness is assigned to it.

That is to say, mypy is catching the following potential runtime error early:

>>> bar("1", "2")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 2, in bar
TypeError: foo() got multiple values for argument 'biz'

The string "1" is assigned to ness and the string "2" becomes the first element of args. When foo is called, the elemnts of args are assigned to the positional parameters biz and baz; in this case only biz gets a value, namely "2". Only then is the keyword argument biz=ness considered, at which point biz has already been accounted for.

Upvotes: 2

Related Questions