@overload Resolution: mypy vs pyright
Both mypy and pyright resolve @overload by trying each signature top to bottom and taking the
first match, so order matters. They diverge on overlap detection (mypy flags unsafe overlaps with
[overload-overlap]; pyright reports them as a separate diagnostic class), on how strictly the
implementation must be compatible with the overloads, and on argument-evaluation order for ambiguous
calls. Order overloads most-specific first and keep the implementation signature broad.
@overload lets one function present several typed signatures to the checker while sharing a single
runtime implementation. The resolution algorithm — try each overload in source order, pick the first
whose parameters match — is shared by mypy and pyright, but the diagnostics they emit when
overloads overlap or the implementation is incompatible differ enough to matter in CI. This guide,
part of Advanced Typing Patterns & Generics, puts the two side
by side. For the basics of declaring overloads, see
Function Overloading.
Versions under test
The behaviour below was observed on mypy 1.10+ and pyright 1.1.360+. The first-match algorithm is stable across both; the diagnostics differ. Pin both in CI per the pyright vs mypy comparison.
Shared rule: first match wins, so order matters
Both checkers evaluate overloads in source order and stop at the first signature whose parameters are compatible with the call. A more general overload placed before a specific one shadows it.
# Python 3.11+, mypy 1.10 AND pyright 1.1.360 — identical resolution
from typing import overload
@overload
def parse(value: int) -> int: ...
@overload
def parse(value: str) -> str: ...
def parse(value: int | str) -> int | str:
return value
reveal_type(parse(3)) # both: int
reveal_type(parse("x")) # both: str
Order most-specific first. bool before int matters because bool is an int subtype: an int
overload listed first would match a bool call and you would never reach the bool overload.
Divergence 1: overlap detection diagnostics
When two overloads overlap such that one could match where a different return type is expected, mypy
reports [overload-overlap] (the message “Overloaded function signatures N and M overlap with
incompatible return types”). pyright reports the same hazard under its own
reportOverlappingOverload diagnostic.
# Python 3.11+
from typing import overload
@overload
def handle(value: int) -> str: ...
@overload
def handle(value: bool) -> int: ... # bool is a subtype of int -> unreachable / overlap
def handle(value: int) -> str | int:
return ""
# mypy 1.10: error: Overloaded function signatures 1 and 2 overlap with incompatible return types [overload-overlap]
# pyright 1.1: error: Overload 2 will never be used because its parameters overlap overload 1 — reportOverlappingOverload
To suppress, mypy targets overload-overlap; pyright targets reportOverlappingOverload. A
# type: ignore[overload-overlap] does nothing for pyright.
Divergence 2: implementation compatibility
mypy checks that the implementation signature is compatible with every overload and reports
[misc] (“Overloaded function implementation does not accept all possible arguments”) or
[overload-impl]-style messages when it is not. pyright performs a comparable check and reports it
as a general overload error message. mypy is generally stricter about the implementation return type
being a supertype of each overload’s return.
# Python 3.11+
from typing import overload
@overload
def first(value: int) -> int: ...
@overload
def first(value: str) -> str: ...
def first(value: int) -> int: # impl omits str -> incompatible with overload 2
return value
# mypy 1.10: error: Overloaded function implementation does not accept all possible arguments of signature 2 [misc]
# pyright 1.1: error: Implementation is not consistent with overload 2
Keep the implementation signature broad — typically the union of all overload inputs — so it accepts every case the overloads promise.
Divergence 3: ambiguous matches and argument types
When a call could match more than one overload because an argument is a union or Any, the checkers
can pick differently. With an Any argument, pyright tends to evaluate against all overloads and may
return the first; mypy has specific rules for Any that can yield Any as the result. Verify with
reveal_type rather than assuming.
# Python 3.11+
from typing import overload, Any
@overload
def widen(value: int) -> int: ...
@overload
def widen(value: str) -> str: ...
def widen(value: int | str) -> int | str:
return value
x: Any
reveal_type(widen(x))
# mypy: int | str (or Any, depending on settings)
# pyright: int (first overload) — confirm in your toolchain
Common mistakes
- General overload before specific: A broad signature placed first shadows the specific one, so it “never matches”. Order most-specific first; pyright will warn the shadowed overload is unused.
- Narrow implementation signature: If the implementation does not accept every overload’s inputs,
mypy reports
[misc]. Type the implementation as the union of all overload parameters. - Suppressing the wrong code:
overload-overlap(mypy) andreportOverlappingOverload(pyright) are different identifiers; suppress each on its own checker. - Single
@overload: One overload plus an implementation is rejected — overloads require at least two decorated signatures.
FAQ
Why does pyright say an overload “will never be used”? An earlier overload already matches every call the later one would, usually because the earlier parameter type is a supertype. Reorder so the more specific overload comes first.
Do mypy and pyright ever pick different overloads for the same call?
For concrete arguments, no — first-match is deterministic and shared. For Any or union arguments
the result can differ; confirm with reveal_type and design overloads so the choice is unambiguous.