Reputation: 31186
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
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
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:
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)
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)
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:
bar_from_question
returns a result, every other bar_...
implementation returns the same result;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
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