Reputation: 5038
I have the following function:
#!/usr/bin/env python3
from typing import Union
def foo(b: Union[str, int]) -> int:
def bar(a: int = b) -> int: # Incompatible default for argument "a" (default has type "Union[str, int]", argument has type "int")
return a + 1
if isinstance(b, str):
return bar(0)
else:
return bar()
print(foo(3)) # 4
print(foo("hello")) # 1
On the line where I define bar
, Mypy says that setting b
as the default won't work.
However, due to how the program works, the only way that the default b
will be used is if b
is an integer. So this should work fine.
But Mypy doesn't realize that.
How can I
int
is the correct type for a
or(For example, I know I could write two foo
functions with different signatures, but that would be too much code duplication.)
TL;DR below is just my real-life use case, since at least one answer relied on how simple my MCVE above was.
It's a function that takes a dictionary. The function returns a decorator that, when used, will add the decorated function (the decorated function is a TypeChecker
) to the dictionary. The decorator allows for a parameter that specifies the name/key that the decorated function (the TypeChecker
) is placed under in the dictionary. If a name is not specified, then it will use a different function (StringHelper.str_function
) to figure out a name from the properties of the function itself.
Due to how decorator parameters work, the decorator creator needs to take in either the name (or nothing) or the function. If it takes just the function, then no name was specified, and it should grab a name from the function. If it takes just the name, then it will be called again on the function, and the name should be used. If it takes nothing, then it will be called again on the function, and it should grab a name from the function.
def get_type_checker_decorator(type_checkers: Dict[str, TypeChecker]) -> Callable[[Union[Optional[str], TypeChecker]], Union[Callable[[TypeChecker], TypeChecker], TypeChecker]]:
@overload
def type_checker_decorator(name: Optional[str]) -> Callable[[TypeChecker], TypeChecker]:
pass
@overload
def type_checker_decorator(name: TypeChecker) -> TypeChecker:
pass
def type_checker_decorator(name: Union[Optional[str], TypeChecker] = None) -> Union[Callable[[TypeChecker], TypeChecker], TypeChecker]:
# if name is a function, then the default will never be used
def inner_decorator(function: TypeChecker, name: Optional[str] = name) -> TypeChecker: # this gives the Mypy error
if name is None:
name = StringHelper.str_function(function)
type_checkers[name] = function
def wrapper(string: str) -> bool:
return function(string)
return wrapper
if callable(name):
# they just gave us the function right away without a name
function = name
name = None
return inner_decorator(function, name)
else:
assert isinstance(name, str) or name is None
# should be called with just the function as a parameter
# the name will default to the given name (which may be None)
return inner_decorator
return type_checker_decorator
Upvotes: 3
Views: 2839
Reputation: 281643
This default value shouldn't even exist. The safe way to write this code would be
def foo(b: Union[str, int]) -> int:
def bar(a) -> int:
return a + 1
if isinstance(b, str):
return bar(0)
else:
return bar(b)
I don't know what situation motivated you to ask this question, but whatever program you really want to write, you probably shouldn't have a default argument value either.
Your code is very similar to trying to do
def f(x: Union[int, str]) -> int:
y: int = x
if isinstance(x, str):
return 1
else:
return y + 1
Written like that, it seems obvious that y
is wrong. We shouldn't be assigning something to a variable of static type int
unless we actually know at the point of assignment that it's an int. It would be unreasonable to expect the type checker to examine all code paths that could lead to y
's use to determine that this is safe. Default argument value type checking follows a similar principle; it's checked based on information available when the default value is set, not on the code paths where it could be used.
For an even more extreme example, consider
def f(x: Union[int, str]) -> int:
def g(y: int = x):
pass
return 4
y
will never be used. The type checker still reports the type mismatch, and there would be bug reports about it if the type checker didn't report it.
Upvotes: 1
Reputation: 12026
It feels awkward to force a type signature if that's not really what the function is expecting. Your bar
function clearly expects an int
, and forcing a Union
on the type hint just to later assert that you actually only accept int
s shouldn't be necessary in order to silence mypy.
Since you are accepting b
as a default in bar, you should take care of the str
case inside of bar
, because the type signature of b
has already been specified in foo
. Two alternative solutions that I would've considered more appropriate to the issue at hand:
def foo(b: Union[str, int]) -> int:
# bar takes care of the str case. Type of b already documented
def bar(a=b) -> int:
if isinstance(b, str):
return bar(0)
return a + 1
return bar()
Defining a default value before defining bar
:
def foo(b: Union[str, int]) -> int:
x: int = 0 if isinstance(b, str) else b
# bar does not take a default type it won't use.
def bar(a: int = x) -> int:
return a + 1
return bar()
Upvotes: 3
Reputation: 5038
While writing this question, I think I answered it myself, so I might as well share.
The solution is slightly awkward, but it's the best I could think of, and it seems to work. First, type a
as being either an integer or a string. Then, as the first line in the function, assert that a
is an int
.
Together, it looks like
#!/usr/bin/env python3
from typing import Union
def foo(b: Union[str, int]) -> int:
def bar(a: Union[int, str] = b) -> int: # Incompatible default for argument "a" (default has type "Union[str, int]", argument has type "int")
assert isinstance(a, int)
return a + 1
if isinstance(b, str):
return bar(0)
else:
return bar()
print(foo(3)) # 4
print(foo("hello")) # 1
This solution isn't perfect, though, since if you do follow the function signature and pass it a string, it will fail (due to the assert
). So it requires some introspection of the function itself to determine what can actually get passed.
Upvotes: 0