I don’t know if it’s been brought up before, but I wonder why is this not currently allowed:
@overload
def foo(a: A1) -> B1:
...
@overload
def foo(a: A2) -> B2:
...
def foo(a):
# actual code here
If given no type hints, the type of foo should be the union of its overloads.
Currently, you allowed to write something like this if you add # type: ignore on the real function, but I have no real idea if the type checker deal with this as well as I’d hope.
When I tried this I got no error from mypy nor from pyright (via VS Code + PyLance). Could you tell us what tool you are using for type checking and what error you got?
OK, I figured it out, this happens because my project uses --strict which enables --disallow-untyped-defs.
I still wonder if it’s right for these checking modes to behave like that when all overloads are typed, though.
But perhaps I should close this topic, as it is clearly mypy-specific behavior.
I personally found typing.overloads unintuitive when I first came across it, I came to realize it’s for specifying an interface for code that is calling the respective function and is not used to check code in the function. Hence why mypy complains (in strict mode) when you don’t specify types in the function signature, because no type checking is being done inside the function for these variables.
I would find overload a lot more useful if there were an option to have the type checker check each overload actually works for the code in the function.
But from my memory of first discovering this and getting confused and going to read various mypy GitHub issues it is currently considered complex to implement and no one has proposed a solution or standard.
It seems the general consensus for the ‘actual implementation’ is either to omit typing, or use Anys,
but I’m hoping there’s a way to express the actual types in the signature?
I can type: ignore as a workaround, but it seems a little unsatisfactory (having come this far).
A naive approach is to union the possible types for each arg,
but that quickly runs in to some quite silly situations, like this:
from typing import overload
@overload
def foo(x: int) -> int:
...
@overload
def foo(x: float, y: float) -> float:
...
# This type signature doesn't really capture the true typing
def foo(x: int | float, y: float | None = None):
if isinstance(x, int) and y is None:
return x
elif isinstance(x, float) and isinstance(y, float):
return x + y
# and no surprise the type checker doesn't flag this as unreachable
return 'I should be unreachable code!'
if __name__ == '__main__':
one = foo(3)
print(one) # 3
two = foo(3.0, 5.0)
print(two) # 8.0
# I don't expect super clever dispatch to cast to floats,
# but a type error would be nice
bad = foo(3, 5)
print(bad) # I should be unreachable code!
Is it possible to express a signature that truly captures the nuance that it is either:
x is int and y is None, or:
x and y are both float.
… and no other combination.
I generally use the Union approach and sometimes Any when that gets too annoying to deal with inside the implementation. You can’t really tell the type checker that only a certain subset of combinations of input types are valid inside the body through the signature.
In your example you could consider switching to assertions so your code only has two branches, rather than three, so you get proper runtime validation in addition to the validation the type checker gives you:
# This type signature doesn't really capture the true typing
def foo(x: int | float, y: float | None = None):
if isinstance(x, int) and y is None:
return x
assert isinstance(x, float)
assert isinstance(y, float)
return x + y
or with three branches if you prefer this:
# This type signature doesn't really capture the true typing
def foo(x: int | float, y: float | None = None):
if isinstance(x, int) and y is None:
return x
elif isinstance(x, float) and isinstance(y, float):
return x + y
raise AssertionError("unreachable")
You could also raise a TypeError with a nice message for invalid argument types, rather than an AssertionError.
I assume (to support the original ask) the type checker would need to recheck the same function multiple times, once for each overload, as none of them support cross-variable constraints (e.g. “a and b are either both ints or both floats”). That’s probably a hard nope from the implementors, sadly.
While doing the more accurate check is indeed more expensive (either of the two methods you mentioned), what I had in mind is just that the type checker should be willing to just infer the suitable union types from all overloads and then check the implementing def based on that, unless you explicitly type it differently.
What’s the point of asking the programmer to maintain these hints on their own…
It’s somewhat violating DRY, in a certain sense.