Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
Pre-compute the signature of each overload before checking via the O(…
…n^2) consistency check. Add a fast-path at the top of the inner loop.
  • Loading branch information
pelson committed Apr 14, 2026
commit 86dc7fea493dc3581a98e746fdb18902e4555d8e
87 changes: 81 additions & 6 deletions mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,7 @@
FunctionLike,
Instance,
LiteralType,
LiteralValue,
NoneType,
Overloaded,
PartialType,
Expand Down Expand Up @@ -911,23 +912,42 @@ def check_overlapping_overloads(self, defn: OverloadedFuncDef) -> None:
impl_type = self.extract_callable_type(inner_type, defn.impl)

is_descriptor_get = defn.info and defn.name == "__get__"

# Pre-extract callable types and literal fingerprints for each overload item.
item_sigs: list[CallableType | None] = []
item_literal_fingerprints: list[LiteralFingerprint] = []
for item in defn.items:
assert isinstance(item, Decorator)
sig = self.extract_callable_type(item.var.type, item)
item_sigs.append(sig)
item_literal_fingerprints.append(
build_literal_fingerprint(sig) if sig is not None else {}
)

for i, item in enumerate(defn.items):
assert isinstance(item, Decorator)
sig1 = self.extract_callable_type(item.var.type, item)
sig1 = item_sigs[i]
if sig1 is None:
continue

for j, item2 in enumerate(defn.items[i + 1 :]):
for j, item2 in enumerate(defn.items[i + 1 :], i + 1):
assert isinstance(item2, Decorator)
sig2 = self.extract_callable_type(item2.var.type, item2)
sig2 = item_sigs[j]
if sig2 is None:
continue

if not are_argument_counts_overlapping(sig1, sig2):
continue

# If there is any argument position where both overloads
# carry a LiteralType with different values they are disjoint.
if literal_args_are_disjoint(
item_literal_fingerprints[i], item_literal_fingerprints[j]
):
continue

if overload_can_never_match(sig1, sig2):
self.msg.overloaded_signature_will_never_match(i + 1, i + j + 2, item2.func)
self.msg.overloaded_signature_will_never_match(i + 1, j + 1, item2.func)
elif not is_descriptor_get:
# Note: we force mypy to check overload signatures in strict-optional mode
# so we don't incorrectly report errors when a user tries typing an overload
Expand All @@ -947,14 +967,14 @@ def check_overlapping_overloads(self, defn: OverloadedFuncDef) -> None:
with state.strict_optional_set(True):
if is_unsafe_overlapping_overload_signatures(sig1, sig2, type_vars):
flip_note = (
j == 0
j == i + 1
and not is_unsafe_overlapping_overload_signatures(
sig2, sig1, type_vars
)
and not overload_can_never_match(sig2, sig1)
)
self.msg.overloaded_signatures_overlap(
i + 1, i + j + 2, flip_note, item.func
i + 1, j + 1, flip_note, item.func
)

if impl_type is not None:
Expand Down Expand Up @@ -8958,6 +8978,61 @@ def detach_callable(typ: CallableType, class_type_vars: list[TypeVarLikeType]) -
return typ.copy_modified(variables=list(typ.variables) + class_type_vars)


# Fingerprint type for literal-disjointness checks: maps argument index to
# the set of (Python type of value, value) pairs present at that position.
# Using type(value) as part of the key means Literal[1] (int) and
# Literal[True] (bool) are kept distinct even though 1 == True in Python.
# A union such as Literal["a", "b"] or Literal["a"] | Literal["b"] produces
# a frozenset of two entries; a plain Literal["a"] produces a singleton set.
LiteralFingerprint = dict[int, frozenset[tuple[type, LiteralValue]]]


def literal_args_are_disjoint(fp1: LiteralFingerprint, fp2: LiteralFingerprint) -> bool:
"""Return True if two overloads are provably disjoint via a Literal argument.

If there is any argument position where both carry only LiteralType values
and those value sets are disjoint, no single call can match both overloads
and the pairwise overlap check can be skipped entirely.
"""
for idx, vals1 in fp1.items():
vals2 = fp2.get(idx)
if vals2 is not None and vals1.isdisjoint(vals2):
return True
return False


def build_literal_fingerprint(sig: CallableType) -> LiteralFingerprint:
"""Build a LiteralFingerprint for one overload signature.

Each argument position that carries only LiteralType values (including
unions such as ``Literal["a", "b"]``) is recorded as a frozenset of
``(type(value), value)`` pairs. Positions with any non-literal type are
omitted so the disjointness check is conservative.
"""
fingerprint: LiteralFingerprint = {}
for idx, arg_type in enumerate(sig.arg_types):
proper = get_proper_type(arg_type)
if isinstance(proper, LiteralType):
fingerprint[idx] = frozenset([(type(proper.value), proper.value)])
elif isinstance(proper, UnionType):
# Literal["a", "b"] and Literal["a"] | Literal["b"] are both
# represented as a UnionType of LiteralTypes. Collect all the
# literal values; if any member is not a LiteralType the whole
# position is skipped (a non-literal in the union makes it too
# broad to prove disjointness).
vals: set[tuple[type, LiteralValue]] = set()
for member in proper.items:
m = get_proper_type(member)
if isinstance(m, LiteralType):
vals.add((type(m.value), m.value))
else:
vals = set()
break
if vals:
fingerprint[idx] = frozenset(vals)
return fingerprint


def overload_can_never_match(signature: CallableType, other: CallableType) -> bool:
"""Check if the 'other' method can never be matched due to 'signature'.

Expand Down
98 changes: 98 additions & 0 deletions test-data/unit/check-overloading.test
Original file line number Diff line number Diff line change
Expand Up @@ -6865,3 +6865,101 @@ if isinstance(headers, dict):

reveal_type(headers) # N: Revealed type is "__main__.Headers | typing.Iterable[tuple[builtins.bytes, builtins.bytes]]"
[builtins fixtures/isinstancelist.pyi]

-- Tests for literal-disjointness fast path in check_overlapping_overloads

[case testOverloadLiteralDistinctStringsNoError]
# Overloads with distinct Literal[str] arguments are provably disjoint; no
# overlap or never-match errors should be reported.
from typing import overload, Literal
@overload
def f(x: Literal["a"]) -> int: ...
@overload
def f(x: Literal["b"]) -> str: ...
@overload
def f(x: Literal["c"]) -> float: ...
def f(x: str) -> object:
return x
[builtins fixtures/tuple.pyi]

[case testOverloadLiteralDuplicateStillErrors]
# Two overloads sharing the same Literal value should still trigger an error.
# Signature 1 covers all inputs of type Literal["a"], so signature 2 is unreachable.
from typing import overload, Literal
@overload
def f(x: Literal["a"]) -> int: ...
@overload
def f(x: Literal["a"]) -> str: ... # E: Overloaded function signature 2 will never be matched: signature 1's parameter type(s) are the same or broader
def f(x: str) -> object:
return x
[builtins fixtures/tuple.pyi]

[case testOverloadLiteralWithBroadCatchAll]
# Distinct Literal overloads followed by a broad catch-all should produce no
# overlap errors. The broad type must come last (correct ordering).
from typing import overload, Literal, Any
@overload
def f(x: Literal["a"]) -> int: ...
@overload
def f(x: Literal["b"]) -> str: ...
@overload
def f(x: str) -> Any: ...
def f(x: str) -> object:
return x
[builtins fixtures/tuple.pyi]

[case testOverloadLiteralBroadBeforeLiteralErrors]
# A broad type before a specific Literal means the Literal can never match.
from typing import overload, Literal
@overload
def f(x: str) -> int: ...
@overload
def f(x: Literal["a"]) -> str: ... # E: Overloaded function signature 2 will never be matched: signature 1's parameter type(s) are the same or broader
def f(x: str) -> object:
return x
[builtins fixtures/tuple.pyi]

[case testOverloadLiteralImplErrorsNotSuppressed]
# The literal fast path must not suppress implementation-body consistency errors.
# Use bytes as the impl return type — incompatible with both int and str.
from typing import overload, Literal
@overload
def f(x: Literal["a"]) -> int: ...
@overload
def f(x: Literal["b"]) -> str: ...
def f(x: str) -> bytes: # E: Overloaded function implementation cannot produce return type of signature 1 # E: Overloaded function implementation cannot produce return type of signature 2
return b""
[builtins fixtures/tuple.pyi]

[case testOverloadLiteralUnionDistinctNoError]
# Literal unions with disjoint value sets are provably disjoint; no errors.
from typing import overload, Literal, Union
@overload
def f(x: Literal["a", "b"]) -> int: ...
@overload
def f(x: Literal["c", "d"]) -> str: ...
def f(x: str) -> object:
return x
[builtins fixtures/tuple.pyi]

[case testOverloadLiteralUnionOverlapErrors]
# Literal unions that share a value are NOT disjoint and should be flagged.
from typing import overload, Literal
@overload
def f(x: Literal["a", "b"]) -> int: ... # E: Overloaded function signatures 1 and 2 overlap with incompatible return types
@overload
def f(x: Literal["b", "c"]) -> str: ...
def f(x: str) -> object:
return x
[builtins fixtures/tuple.pyi]

[case testOverloadLiteralUnionMixedNoFastPath]
# A union with a non-Literal member is not fingerprinted, so the full check runs.
from typing import overload, Literal, Union
@overload
def f(x: Literal["a"]) -> int: ... # E: Overloaded function signatures 1 and 2 overlap with incompatible return types
@overload
def f(x: Union[Literal["b"], str]) -> str: ...
def f(x: str) -> object:
return x
[builtins fixtures/tuple.pyi]