crates/ty_python_semantic/resources/mdtest/overloads.md
Reference: https://typing.python.org/en/latest/spec/overload.html
typing.overloadThe definition of typing.overload in typeshed is an identity function.
from typing import Callable, overload
def foo(x: int) -> int:
return x
reveal_type(foo) # revealed: def foo(x: int) -> int
bar = overload(foo)
reveal_type(bar) # revealed: def foo(x: int) -> int
from typing import Callable, overload
@overload
def add() -> None: ...
@overload
def add(x: int) -> int: ...
@overload
def add(x: int, y: int) -> int: ...
def add(x: int | None = None, y: int | None = None) -> int | None:
return (x or 0) + (y or 0)
reveal_type(add) # revealed: Overload[() -> None, (x: int) -> int, (x: int, y: int) -> int]
reveal_type(add()) # revealed: None
reveal_type(add(1)) # revealed: int
reveal_type(add(1, 2)) # revealed: int
These scenarios are to verify that the overloaded and non-overloaded definitions are correctly overridden by each other.
An overloaded function is overriding another overloaded function:
from typing import overload
@overload
def foo() -> None: ...
@overload
def foo(x: int) -> int: ...
def foo(x: int | None = None) -> int | None:
return x
reveal_type(foo) # revealed: Overload[() -> None, (x: int) -> int]
reveal_type(foo()) # revealed: None
reveal_type(foo(1)) # revealed: int
@overload
def foo() -> None: ...
@overload
def foo(x: str) -> str: ...
def foo(x: str | None = None) -> str | None:
return x
reveal_type(foo) # revealed: Overload[() -> None, (x: str) -> str]
reveal_type(foo()) # revealed: None
reveal_type(foo("")) # revealed: str
A non-overloaded function is overriding an overloaded function:
def foo(x: int) -> int:
return x
reveal_type(foo) # revealed: def foo(x: int) -> int
An overloaded function is overriding a non-overloaded function:
reveal_type(foo) # revealed: def foo(x: int) -> int
@overload
def foo() -> None: ...
@overload
def foo(x: bytes) -> bytes: ...
def foo(x: bytes | None = None) -> bytes | None:
return x
reveal_type(foo) # revealed: Overload[() -> None, (x: bytes) -> bytes]
reveal_type(foo()) # revealed: None
reveal_type(foo(b"")) # revealed: bytes
from typing_extensions import Self, overload
class Foo1:
@overload
def method(self) -> None: ...
@overload
def method(self, x: int) -> int: ...
def method(self, x: int | None = None) -> int | None:
return x
foo1 = Foo1()
reveal_type(foo1.method) # revealed: Overload[() -> None, (x: int) -> int]
reveal_type(foo1.method()) # revealed: None
reveal_type(foo1.method(1)) # revealed: int
class Foo2:
@overload
def method(self) -> None: ...
@overload
def method(self, x: str) -> str: ...
def method(self, x: str | None = None) -> str | None:
return x
foo2 = Foo2()
reveal_type(foo2.method) # revealed: Overload[() -> None, (x: str) -> str]
reveal_type(foo2.method()) # revealed: None
reveal_type(foo2.method("")) # revealed: str
class Foo3:
@overload
def takes_self_or_int(self: Self, x: Self) -> Self: ...
@overload
def takes_self_or_int(self: Self, x: int) -> int: ...
def takes_self_or_int(self: Self, x: Self | int) -> Self | int:
return x
foo3 = Foo3()
reveal_type(foo3.takes_self_or_int(foo3)) # revealed: Foo3
reveal_type(foo3.takes_self_or_int(1)) # revealed: int
Binding a method filters overloads that explicitly annotate self with a type that cannot accept
the bound receiver. The Child-specific overload is therefore unavailable when accessing the method
through Base, but is retained for Child.
from typing import overload
class Base:
@overload
def narrowed(self: "Base", x: int) -> int: ...
@overload
def narrowed(self: "Child", x: str) -> str: ...
def narrowed(self, x: int | str) -> int | str:
return x
class Child(Base): ...
reveal_type(Base().narrowed) # revealed: bound method Base.narrowed(x: int) -> int
reveal_type(Child().narrowed) # revealed: Overload[(x: int) -> int, (x: str) -> str]
An explicit receiver annotation can also select overloads based on a generic class specialization.
[environment]
python-version = "3.12"
from typing import overload
class Box[T]:
# Use the class type parameter so specializations remain meaningful.
x: T
@overload
def specialized(self: "Box[str]", x: str) -> str: ...
@overload
def specialized(self, x: int) -> int: ...
def specialized(self, x: str | int) -> str | int:
return x
reveal_type(Box[int]().specialized) # revealed: bound method Box[int].specialized(x: int) -> int
reveal_type(Box[str]().specialized) # revealed: Overload[(x: str) -> str, (x: int) -> int]
BaseForAny[Any] has a dynamic type argument, but it is not necessarily an instance of its subclass
ChildForAny. Binding g must still remove the overload restricted to that subclass.
[environment]
python-version = "3.12"
from typing import Any, Callable, overload
class BaseForAny[T]:
value: T
@overload
def g(self: "ChildForAny") -> bytes: ...
@overload
def g(self) -> int: ...
def g(self) -> bytes | int:
return 1
class ChildForAny(BaseForAny[int]): ...
def accepts_bytes_callback(cb: Callable[[], bytes]) -> None: ...
def takes_base_any(base: BaseForAny[Any]) -> None:
reveal_type(base.g) # revealed: bound method BaseForAny[Any].g() -> int
reveal_type(base.g()) # revealed: int
accepts_bytes_callback(base.g) # error: [invalid-argument-type]
When none of the explicitly annotated receivers accept the bound object, no overload is exposed.
[environment]
python-version = "3.12"
from typing import Callable, overload
class BaseWithNoMatchingReceiver[T]:
value: T
@overload
def g(self: "BaseWithNoMatchingReceiver[bytes]") -> bytes: ...
@overload
def g(self: "BaseWithNoMatchingReceiver[str]") -> str: ...
def g(self) -> str | bytes:
return ""
def takes_str_callback(cb: Callable[[], str]) -> None: ...
def no_matching_receiver(y: BaseWithNoMatchingReceiver[int]) -> None:
reveal_type(y.g) # revealed: Overload[]
takes_str_callback(y.g) # error: [invalid-argument-type]
A receiver specialized with a union is not specifically a Reader[int] or a Reader[str], so
neither restricted overload is exposed.
from typing import Generic, TypeVar, overload
T_co = TypeVar("T_co", covariant=True)
class Reader(Generic[T_co]):
@overload
def get(self: "Reader[int]", default: int) -> int: ...
@overload
def get(self: "Reader[str]", default: str) -> str: ...
def get(self, default: object) -> object:
return default
def union_receiver(reader: Reader[int | str]):
reveal_type(reader.get) # revealed: Overload[]
selfBinding an overload whose explicit receiver introduces a method type variable should infer that
variable from the concrete receiver and apply it to the remainder of the signature. At present,
receiver matching retains the overload, but does not yet apply the inferred S = str
specialization.
[environment]
python-version = "3.12"
from typing import overload
class ReceiverGeneric[T]:
@overload
def method[S](self: "ReceiverGeneric[S]", value: S) -> S: ...
@overload
def method(self, value: bytes) -> bytes: ...
def method(self, value: object) -> object:
return value
# TODO: `Signature::can_bind_self_to` currently reduces receiver matching to a boolean. Instead,
# each retained overload should preserve its receiver constraints so later specialization can apply
# them.
# TODO: revealed: Overload[(value: str) -> str, (value: bytes) -> bytes]
reveal_type(ReceiverGeneric[str]().method) # revealed: Overload[[S](value: S) -> S, (value: bytes) -> bytes]
Checking a generic protocol receiver requires solving all uses of its type variable together. Here
get() would require int to be assignable to T, while put() would require T to be
assignable to str, so no T can satisfy ProtocolSelf[T]. At present, the incompatible overload
is retained because structural receiver specialization is not yet supported.
from typing import Callable, Protocol, TypeVar, overload
ProtocolSelfT = TypeVar("ProtocolSelfT")
class ProtocolSelf(Protocol[ProtocolSelfT]):
def get(self) -> ProtocolSelfT: ...
def put(self, x: ProtocolSelfT) -> None: ...
class BaseWithProtocolSelf:
@overload
def method(self: ProtocolSelf[ProtocolSelfT]) -> ProtocolSelfT: ...
@overload
def method(self) -> bytes: ...
def method(self) -> object:
return b""
class ProtocolSelfImplementation(BaseWithProtocolSelf):
def get(self) -> int:
return 1
def put(self, x: str) -> None: ...
# TODO: The first overload should be eliminated, leaving `bound method
# BaseWithProtocolSelf.method() -> bytes`.
reveal_type(ProtocolSelfImplementation().method) # revealed: Overload[[ProtocolSelfT]() -> ProtocolSelfT, () -> bytes]
good_protocol_receiver: Callable[[], bytes] = ProtocolSelfImplementation().method
# TODO: error: [invalid-assignment]
bad_protocol_receiver: Callable[[], int] = ProtocolSelfImplementation().method
from typing import overload
class Foo:
@overload
def __init__(self) -> None: ...
@overload
def __init__(self, x: int) -> None: ...
def __init__(self, x: int | None = None) -> None:
self.x = x
foo = Foo()
reveal_type(foo) # revealed: Foo
reveal_type(foo.x) # revealed: int | None
foo1 = Foo(1)
reveal_type(foo1) # revealed: Foo
reveal_type(foo1.x) # revealed: int | None
Function definitions can vary between multiple Python versions.
Here, the same function is overloaded in one version and not in another.
[environment]
python-version = "3.9"
from __future__ import annotations
import sys
from typing import overload
if sys.version_info < (3, 10):
def func(x: int) -> int:
return x
elif sys.version_info <= (3, 12):
@overload
def func() -> None: ...
@overload
def func(x: int) -> int: ...
def func(x: int | None = None) -> int | None:
return x
reveal_type(func) # revealed: def func(x: int) -> int
func() # error: [missing-argument]
[environment]
python-version = "3.10"
import sys
from typing import overload
if sys.version_info < (3, 10):
def func(x: int) -> int:
return x
elif sys.version_info <= (3, 12):
@overload
def func() -> None: ...
@overload
def func(x: int) -> int: ...
def func(x: int | None = None) -> int | None:
return x
reveal_type(func) # revealed: Overload[() -> None, (x: int) -> int]
reveal_type(func()) # revealed: None
reveal_type(func(1)) # revealed: int
[environment]
python-version = "3.9"
overloaded.pyi:
import sys
from typing import overload
if sys.version_info >= (3, 10):
@overload
def func() -> None: ...
@overload
def func(x: int) -> int: ...
@overload
def func(x: str) -> str: ...
main.py:
from overloaded import func
reveal_type(func) # revealed: Overload[(x: int) -> int, (x: str) -> str]
func() # error: [no-matching-overload]
reveal_type(func(1)) # revealed: int
reveal_type(func("")) # revealed: str
[environment]
python-version = "3.10"
overloaded.pyi:
import sys
from typing import overload
@overload
def func() -> None: ...
if sys.version_info >= (3, 10):
@overload
def func(x: int) -> int: ...
@overload
def func(x: str) -> str: ...
main.py:
from overloaded import func
reveal_type(func) # revealed: Overload[() -> None, (x: int) -> int, (x: str) -> str]
reveal_type(func()) # revealed: None
reveal_type(func(1)) # revealed: int
reveal_type(func("")) # revealed: str
[environment]
python-version = "3.12"
For an overloaded generic function, it's not necessary for all overloads to be generic.
from typing import overload
@overload
def func() -> None: ...
@overload
def func[T](x: T) -> T: ...
def func[T](x: T | None = None) -> T | None:
return x
reveal_type(func) # revealed: Overload[() -> None, [T](x: T) -> T]
reveal_type(func()) # revealed: None
reveal_type(func(1)) # revealed: Literal[1]
reveal_type(func("")) # revealed: Literal[""]
At least two @overload-decorated definitions must be present.
from typing import overload
@overload
# error: [invalid-overload]
def func(x: int) -> int: ...
def func(x: int | str) -> int | str:
return x
from typing import overload
@overload
# error: [invalid-overload]
def func(x: int) -> int: ...
In regular modules, a series of @overload-decorated definitions must be followed by exactly one
non-@overload-decorated definition (for the same function/method).
from typing import overload
@overload
# error: [invalid-overload] "Overloads for function `func` must be followed by a non-`@overload`-decorated implementation function"
def func(x: int) -> int: ...
@overload
def func(x: str) -> str: ...
class Foo:
@overload
# error: [invalid-overload] "Overloads for function `method` must be followed by a non-`@overload`-decorated implementation function"
def method(self, x: int) -> int: ...
@overload
def method(self, x: str) -> str: ...
Overload definitions within stub files are exempt from this check.
from typing import overload
@overload
def func(x: int) -> int: ...
@overload
def func(x: str) -> str: ...
Overload definitions within protocols are exempt from this check.
from typing import Protocol, overload
class Foo(Protocol):
@overload
def f(self, x: int) -> int: ...
@overload
def f(self, x: str) -> str: ...
Overload definitions within abstract base classes are exempt from this check.
from abc import ABC, abstractmethod
from typing import overload
class AbstractFoo(ABC):
@overload
@abstractmethod
def f(self, x: int) -> int: ...
@overload
@abstractmethod
def f(self, x: str) -> str: ...
Using the @abstractmethod decorator requires that the class's metaclass is ABCMeta or is derived
from it.
from abc import ABCMeta
class CustomAbstractMetaclass(ABCMeta): ...
class Fine(metaclass=CustomAbstractMetaclass):
@overload
@abstractmethod
def f(self, x: int) -> int: ...
@overload
@abstractmethod
def f(self, x: str) -> str: ...
class Foo:
@overload
@abstractmethod
# error: [invalid-overload]
def f(self, x: int) -> int: ...
@overload
@abstractmethod
def f(self, x: str) -> str: ...
And, the @abstractmethod decorator must be present on all the @overload-ed methods.
class PartialFoo1(ABC):
@overload
@abstractmethod
# error: [invalid-overload]
def f(self, x: int) -> int: ...
@overload
def f(self, x: str) -> str: ...
class PartialFoo(ABC):
@overload
# error: [invalid-overload]
def f(self, x: int) -> int: ...
@overload
@abstractmethod
def f(self, x: str) -> str: ...
TYPE_CHECKING blocksAs in other areas of ty, we treat TYPE_CHECKING blocks the same as "inline stub files", so we
permit overloaded functions to exist without an implementation if all overloads are defined inside
an if TYPE_CHECKING block:
from typing import overload, TYPE_CHECKING
if TYPE_CHECKING:
@overload
def a() -> str: ...
@overload
def a(x: int) -> int: ...
class F:
@overload
def method(self) -> None: ...
@overload
def method(self, x: int) -> int: ...
class G:
if TYPE_CHECKING:
@overload
def method(self) -> None: ...
@overload
def method(self, x: int) -> int: ...
if TYPE_CHECKING:
@overload
def b() -> str: ...
if TYPE_CHECKING:
@overload
def b(x: int) -> int: ...
if TYPE_CHECKING:
import sys
if sys.platform == "win32":
pass
else:
@overload
def d() -> bytes: ...
@overload
def d(x: int) -> int: ...
if TYPE_CHECKING:
@overload
# not all overloads are in a `TYPE_CHECKING` block, so this is an error
def c() -> None: ... # error: [invalid-overload]
@overload
def c(x: int) -> int: ...
@overload-decorated functions with non-stub bodiesIf an @overload-decorated function has a non-trivial body, it likely indicates a misunderstanding
on the part of the user. We emit a warning-level diagnostic to alert them of this.
..., pass and docstrings are all fine:
from typing import overload
@overload
def x(y: int) -> int: ...
@overload
def x(y: str) -> str:
"""Docstring"""
@overload
def x(y: bytes) -> bytes:
pass
@overload
def x(y: memoryview) -> memoryview:
"""More docs"""
pass
...
def x(y):
return y
Anything else, however, will trigger the lint:
@overload
def foo(x: int) -> int:
return x # error: [useless-overload-body]
@overload
def foo(x: str) -> None:
"""Docstring"""
pass
print("oh no, a string") # error: [useless-overload-body]
def foo(x):
return x
The implementation must accept every argument accepted by each overload, and each overload return type must be assignable to the implementation return type. This check initially only covers overloads where all signatures are non-generic.
from typing import overload
@overload
def return_type(x: int) -> int: ...
@overload
# error: [invalid-overload] "Overload return type is not assignable to implementation return type"
def return_type(x: str) -> str: ...
def return_type(x: int | str) -> int:
return 1
@overload
def parameter_type(x: int) -> int: ...
@overload
# error: [invalid-overload] "Implementation does not accept all arguments of this overload"
def parameter_type(x: str) -> str: ...
def parameter_type(x: int) -> int | str:
return 1
Generic overloads are left to the full implementation-consistency check.
from typing import TypeVar, overload
T = TypeVar("T")
@overload
def generic_parameter_type(x: T) -> T: ...
@overload
def generic_parameter_type(x: str) -> str: ...
def generic_parameter_type(x: int) -> int | str:
return x
Non-generic implementation checks require parameter names and positional-only forms to line up with each overload signature.
[environment]
python-version = "3.12"
from collections.abc import Iterable
from typing import overload
class ColumnSelector:
@overload
def _extract(self, row_key: int) -> object: ...
@overload
# snapshot: invalid-overload
def _extract(self, column_key: int) -> object: ...
def _extract(self, row_key: int | None = None, column_key: int | None = None) -> object:
return object()
class PositionalOnlyWithKwargs:
@overload
def update(self, params: Iterable[tuple[str, str | Iterable[str]]], /, **kwds: str) -> None: ...
@overload
# snapshot: invalid-overload
def update(self, **kwds: str | Iterable[str]) -> None: ...
def update(self, params=(), /, **kwds) -> None:
pass
error[invalid-overload]: Implementation does not accept all arguments of this overload
--> src/mdtest_snippet.py:9:9
|
9 | def _extract(self, column_key: int) -> object: ...
| ^^^^^^^^
10 | def _extract(self, row_key: int | None = None, column_key: int | None = None) -> object:
| -------- Implementation defined here
|
info: Implementation signature `(self, row_key: int | None = None, column_key: int | None = None) -> object` is not assignable to overload signature `(self, column_key: int) -> object`
info: the parameter named `row_key` does not match `column_key` (and can be used as a keyword parameter)
error[invalid-overload]: Implementation does not accept all arguments of this overload
--> src/mdtest_snippet.py:18:9
|
18 | def update(self, **kwds: str | Iterable[str]) -> None: ...
| ^^^^^^
19 | def update(self, params=(), /, **kwds) -> None:
| ------ Implementation defined here
|
info: Implementation signature `(self, params=..., /, **kwds) -> None` is not assignable to overload signature `(self, **kwds: Iterable[str]) -> None`
info: parameter `self` is positional-only but must also accept keyword arguments
Non-generic implementation checks show why an overload return type is not assignable to the implementation return type.
[environment]
python-version = "3.12"
from typing import overload
@overload
# snapshot: invalid-overload
def return_tuple(x: int) -> tuple[str]: ...
@overload
def return_tuple(x: str) -> tuple[int]: ...
def return_tuple(x: int | str) -> tuple[int]:
return (1,)
error[invalid-overload]: Overload return type is not assignable to implementation return type
--> src/mdtest_snippet.py:5:5
|
5 | def return_tuple(x: int) -> tuple[str]: ...
| ^^^^^^^^^^^^
6 | @overload
7 | def return_tuple(x: str) -> tuple[int]: ...
8 | def return_tuple(x: int | str) -> tuple[int]:
| ------------ Implementation defined here
|
info: Overload returns `tuple[str]`, which is not assignable to implementation return type `tuple[int]`
info: the first tuple element is not compatible: `str` is not assignable to `int`
@staticmethodIf one overload signature is decorated with @staticmethod, all overload signatures must be
similarly decorated. The implementation, if present, must also have a consistent decorator.
from __future__ import annotations
from typing import Callable, overload
class CheckStaticMethod:
@overload
def method1(x: int) -> int: ...
@overload
def method1(x: str) -> str: ...
@staticmethod
# error: [invalid-overload] "Overloaded function `method1` does not use the `@staticmethod` decorator consistently"
def method1(x: int | str) -> int | str:
return x
@overload
def method2(x: int) -> int: ...
@overload
@staticmethod
def method2(x: str) -> str: ...
@staticmethod
# error: [invalid-overload]
def method2(x: int | str) -> int | str:
return x
@overload
@staticmethod
def method3(x: int) -> int: ...
@overload
@staticmethod
def method3(x: str) -> str: ...
# error: [invalid-overload]
def method3(x: int | str) -> int | str:
return x
@overload
@staticmethod
def method4(x: int) -> int: ...
@overload
@staticmethod
def method4(x: str) -> str: ...
@staticmethod
def method4(x: int | str) -> int | str:
return x
@classmethodThe same rules apply for @classmethod as for @staticmethod.
from __future__ import annotations
from typing import Callable, overload
class CheckClassMethod:
def __init__(self, x: int) -> None:
self.x = x
@overload
@classmethod
def try_from1(cls, x: int) -> CheckClassMethod: ...
@overload
def try_from1(cls, x: str) -> None: ...
@classmethod
# error: [invalid-overload] "Overloaded function `try_from1` does not use the `@classmethod` decorator consistently"
def try_from1(cls, x: int | str) -> CheckClassMethod | None:
if isinstance(x, int):
return cls(x)
return None
@overload
def try_from2(cls, x: int) -> CheckClassMethod: ...
@overload
@classmethod
def try_from2(cls, x: str) -> None: ...
@classmethod
# error: [invalid-overload]
def try_from2(cls, x: int | str) -> CheckClassMethod | None:
if isinstance(x, int):
return cls(x)
return None
@overload
@classmethod
def try_from3(cls, x: int) -> CheckClassMethod: ...
@overload
@classmethod
def try_from3(cls, x: str) -> None: ...
# error: [invalid-overload]
def try_from3(cls, x: int | str) -> CheckClassMethod | None:
if isinstance(x, int):
# error: [call-non-callable]
return cls(x)
return None
@overload
@classmethod
def try_from4(cls, x: int) -> CheckClassMethod: ...
@overload
@classmethod
def try_from4(cls, x: str) -> None: ...
@classmethod
def try_from4(cls, x: int | str) -> CheckClassMethod | None:
if isinstance(x, int):
return cls(x)
return None
class Base:
@overload
@classmethod
def from_value(cls: type[Base], x: int) -> int: ...
@overload
@classmethod
def from_value(cls: type[Child], x: str) -> str: ...
@classmethod
def from_value(cls, x: int | str) -> int | str:
return x
class Child(Base): ...
reveal_type(Base.from_value) # revealed: bound method <class 'Base'>.from_value(x: int) -> int
reveal_type(Child.from_value) # revealed: Overload[(x: int) -> int, (x: str) -> str]
good: Callable[[int], int] = Base.from_value
# error: [invalid-assignment]
bad: Callable[[str], str] = Base.from_value
@finalIf a @final decorator is supplied for a function with overloads, the decorator should be applied
only to the overload implementation if it is present.
from typing_extensions import final, overload
class Foo:
@overload
def method1(self, x: int) -> int: ...
@overload
def method1(self, x: str) -> str: ...
@final
def method1(self, x: int | str) -> int | str:
return x
@overload
@final
# error: [invalid-overload]
def method2(self, x: int) -> int: ...
@overload
def method2(self, x: str) -> str: ...
def method2(self, x: int | str) -> int | str:
return x
@overload
def method3(self, x: int) -> int: ...
@overload
@final
# error: [invalid-overload]
def method3(self, x: str) -> str: ...
def method3(self, x: int | str) -> int | str:
return x
If an overload implementation isn't present (for example, in a stub file), the @final decorator
should be applied only to the first overload.
from typing_extensions import final, overload
class Foo:
@overload
@final
def method1(self, x: int) -> int: ...
@overload
def method1(self, x: str) -> str: ...
@overload
def method2(self, x: int) -> int: ...
@final
@overload
# error: [invalid-overload]
def method2(self, x: str) -> str: ...
@overload
def method3(self, x: int) -> int: ...
@final
@overload
def method3(self, x: str) -> int: ... # error: [invalid-overload]
@overload
@final
def method3(self, x: bytes) -> bytes: ... # error: [invalid-overload]
@overload
def method3(self, x: bytearray) -> bytearray: ...
@overrideThe same rules apply for @override as for @final.
from typing_extensions import overload, override
class Base:
@overload
def method(self, x: int) -> int: ...
@overload
def method(self, x: str) -> str: ...
def method(self, x: int | str) -> int | str:
return x
class Sub1(Base):
@overload
def method(self, x: int) -> int: ...
@overload
def method(self, x: str) -> str: ...
@override
def method(self, x: int | str) -> int | str:
return x
class Sub2(Base):
@overload
def method(self, x: int) -> int: ...
@overload
@override
# error: [invalid-overload]
def method(self, x: str) -> str: ...
def method(self, x: int | str) -> int | str:
return x
class Sub3(Base):
@overload
@override
# error: [invalid-overload]
def method(self, x: int) -> int: ...
@overload
def method(self, x: str) -> str: ...
def method(self, x: int | str) -> int | str:
return x
And, similarly, in stub files:
from typing_extensions import overload, override
class Base:
@overload
def method(self, x: int) -> int: ...
@overload
def method(self, x: str) -> str: ...
class Sub1(Base):
@overload
@override
def method(self, x: int) -> int: ...
@overload
def method(self, x: str) -> str: ...
class Sub2(Base):
@overload
def method(self, x: int) -> int: ...
@overload
@override
# error: [invalid-overload]
def method(self, x: str) -> str: ...
def statement shadows a non-def symbol with the same nameWe used to panic on snippets like these (see https://github.com/astral-sh/ty/issues/1867), because
"iterating over the overloads" for the def statement would incorrectly list the overloads of the
imported function.
module.pyi:
from typing import overload
@overload
def f() -> int: ...
@overload
def f(x) -> str: ...
@overload
def g() -> int: ...
@overload
def g(x) -> str: ...
main.py:
import module
foo = module.f
# revealed: Overload[() -> int, (x) -> str]
reveal_type(foo)
def foo(): ...
# revealed: def foo() -> Unknown
reveal_type(foo)
bar = module.g
# revealed: Overload[() -> int, (x) -> str]
reveal_type(bar)
@staticmethod
def bar(): ...
# revealed: def bar() -> Unknown
reveal_type(bar)
We used to panic on snippets like this, because the post-inference overload checks for the local a
definition would resolve the end-of-scope public binding to the imported f overload set, then try
to render the f definitions using the current file's AST.
module.pyi:
from typing import overload
@overload
def f(x: int) -> int: ...
@overload
def f(x: str) -> str: ...
main.py:
from typing import overload
from module import f
def flag() -> bool:
return True
if flag():
@overload
def a(): ...
else:
a = f
reveal_type(a) # revealed: (def a() -> Unknown) | (Overload[(x: int) -> int, (x: str) -> str])
module.pyi:
from typing import overload
@overload
def f(x: int) -> int: ...
@overload
def f(x: str) -> str: ...
main.py:
from module import f
def flag() -> bool:
return True
if flag():
g = f
else:
def g(x: bytes) -> bytes:
return x
reveal_type(g) # revealed: (Overload[(x: int) -> int, (x: str) -> str]) | (def g(x: bytes) -> bytes)
from typing import overload
def flag() -> bool:
return True
if flag():
@overload
def a(x: int) -> int: ...
@overload
def a(x: str) -> str: ...
@overload
# error: [invalid-overload] "Overloads for function `a` must be followed by a non-`@overload`-decorated implementation function"
def a(x: bytes) -> bytes: ...
@overload
def a(x: bool) -> bool: ...
def statement shadows a non-def symbol with the same name, defined in the same scopeThis is an even more pathological version of the above test. This version used to fail in the same
way as the above snippet, but would only fail in a stub file, or in a .py file that had an
overloaded function without an implementation. (Note that this is not always invalid even in .py
files: we allow overloaded functions to omit the implementation function if they are decorated with
@abstractmethod or they are defined in if TYPE_CHECKING blocks.)
from typing import overload
@overload
def h() -> int: ...
@overload
def h(x) -> str: ...
baz = h
# revealed: Overload[() -> int, (x) -> str]
reveal_type(baz)
# This function is distinct from `h`, despite `h` originating
# from the same scope and being aliased to the same name
# in the same scope!
@overload
def baz(x, y) -> bytes: ...
@overload
def baz(x, y, z) -> list[str]: ...
def baz(x, y, z=None) -> bytes | list[str]:
return b""
# revealed: Overload[(x, y) -> bytes, (x, y, z) -> list[str]]
reveal_type(baz)