From a8f3b80946ac7debf7b5c764bce92da6cf53f4c0 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sat, 19 Sep 2026 21:55:58 +0100 Subject: [PATCH 1/2] Handle edge case in overload subtyping --- mypy/checkexpr.py | 118 ++++---------------------- mypy/subtypes.py | 101 +++++++++++++++++++++- test-data/unit/check-overloading.test | 45 ++++++++++ 3 files changed, 160 insertions(+), 104 deletions(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 647116706d324..d793797a445d9 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -126,7 +126,9 @@ find_member, is_same_type, is_subtype, + merge_typevars_in_callables_by_name, non_method_protocol_members, + union_function_signatures, ) from mypy.traverser import ( all_name_and_member_expressions, @@ -3298,7 +3300,7 @@ def type_overrides_set( def combine_function_signatures(self, types: list[ProperType]) -> AnyType | CallableType: """Accepts a list of function signatures and attempts to combine them together into a - new CallableType consisting of the union of all of the given arguments and return types. + new CallableType consisting of the union of all the given arguments and return types. If there is at least one non-callable type, return Any (this can happen if there is an ambiguity because of Any in arguments). @@ -3306,67 +3308,22 @@ def combine_function_signatures(self, types: list[ProperType]) -> AnyType | Call assert types, "Trying to merge no callables" if not all(isinstance(c, CallableType) for c in types): return AnyType(TypeOfAny.special_form) - callables = cast("list[CallableType]", types) - if len(callables) == 1: - return callables[0] - - # Note: we are assuming here that if a user uses some TypeVar 'T' in - # two different functions, they meant for that TypeVar to mean the - # same thing. - # - # This function will make sure that all instances of that TypeVar 'T' - # refer to the same underlying TypeVarType objects to simplify the union-ing - # logic below. - # - # (If the user did *not* mean for 'T' to be consistently bound to the - # same type in their overloads, well, their code is probably too - # confusing and ought to be re-written anyways.) - callables, variables = merge_typevars_in_callables_by_name(callables) - new_args: list[list[Type]] = [[] for _ in callables[0].arg_types] - new_kinds = list(callables[0].arg_kinds) - new_returns: list[Type] = [] - too_complex = False - - for target in callables: - # We fall back to Callable[..., Union[]] if the functions do not have - # the exact same signature. The only exception is if one arg is optional and - # the other is positional: in that case, we continue unioning (and expect a - # positional arg). - # TODO: Enhance the merging logic to handle a wider variety of signatures. - if len(new_kinds) != len(target.arg_kinds): - too_complex = True - break - for i, (new_kind, target_kind) in enumerate(zip(new_kinds, target.arg_kinds)): - if new_kind == target_kind: - continue - if new_kind.is_positional() and target_kind.is_positional(): - new_kinds[i] = ARG_POS - else: - too_complex = True - break - if too_complex: - break - for i, arg in enumerate(target.arg_types): - new_args[i].append(arg) - new_returns.append(target.ret_type) - - union_return = make_simplified_union(new_returns) - if too_complex: - any = AnyType(TypeOfAny.special_form) - return callables[0].copy_modified( - arg_types=[any, any], - arg_kinds=[ARG_STAR, ARG_STAR2], - arg_names=[None, None], - ret_type=union_return, - variables=variables, - implicit=True, - ) + combined = union_function_signatures(callables) + if combined is not None: + return combined + # We fall back to Callable[..., Union[]] if the functions do not have + # the exact same signature. The only exception is if one arg is optional and + # the other is positional (and expect a positional arg). + callables, variables = merge_typevars_in_callables_by_name(callables) + union_return = make_simplified_union([c.ret_type for c in callables]) + any = AnyType(TypeOfAny.special_form) return callables[0].copy_modified( - arg_types=[make_simplified_union(args) for args in new_args], - arg_kinds=new_kinds, + arg_types=[any, any], + arg_kinds=[ARG_STAR, ARG_STAR2], + arg_names=[None, None], ret_type=union_return, variables=variables, implicit=True, @@ -6965,51 +6922,6 @@ def all_same_types(types: list[Type]) -> bool: return all(is_same_type(t, types[0]) for t in types[1:]) -def merge_typevars_in_callables_by_name( - callables: Sequence[CallableType], -) -> tuple[list[CallableType], list[TypeVarType]]: - """Takes all the typevars present in the callables and 'combines' the ones with the same name. - - For example, suppose we have two callables with signatures "f(x: T, y: S) -> T" and - "f(x: List[Tuple[T, S]]) -> Tuple[T, S]". Both callables use typevars named "T" and - "S", but we treat them as distinct, unrelated typevars. (E.g. they could both have - distinct ids.) - - If we pass in both callables into this function, it returns a list containing two - new callables that are identical in signature, but use the same underlying TypeVarType - for T and S. - - This is useful if we want to take the output lists and "merge" them into one callable - in some way -- for example, when unioning together overloads. - - Returns both the new list of callables and a list of all distinct TypeVarType objects used. - """ - output: list[CallableType] = [] - unique_typevars: dict[str, TypeVarType] = {} - variables: list[TypeVarType] = [] - - for target in callables: - if target.is_generic(): - target = freshen_function_type_vars(target) - - rename = {} # Dict[TypeVarId, TypeVar] - for tv in target.variables: - name = tv.fullname - if name not in unique_typevars: - # TODO: support ParamSpecType and TypeVarTuple. - if isinstance(tv, (ParamSpecType, TypeVarTupleType)): - continue - assert isinstance(tv, TypeVarType) - unique_typevars[name] = tv - variables.append(tv) - rename[tv.id] = unique_typevars[name] - - target = expand_type(target, rename) - output.append(target) - - return output, variables - - def try_getting_literal(typ: Type) -> ProperType: """If possible, get a more precise literal type for a given type.""" typ = get_proper_type(typ) diff --git a/mypy/subtypes.py b/mypy/subtypes.py index 2d97dd534d9f5..0b7a3fcd768cc 100644 --- a/mypy/subtypes.py +++ b/mypy/subtypes.py @@ -1,6 +1,6 @@ from __future__ import annotations -from collections.abc import Callable, Iterable, Iterator +from collections.abc import Callable, Iterable, Iterator, Sequence from contextlib import contextmanager from typing import Any, Final, TypeAlias as _TypeAlias, TypeVar, cast @@ -20,6 +20,7 @@ # Circular import; done in the function instead. # import mypy.solve from mypy.nodes import ( + ARG_POS, ARG_STAR, ARG_STAR2, CONTRAVARIANT, @@ -1013,6 +1014,12 @@ def visit_overloaded(self, left: Overloaded) -> bool: for item in left.items: if self._is_subtype(item, right): return True + # If simple logic failed, check a (somewhat ad hoc but important) + # edge case: Overloaded(def (int) -> int, def (str) -> str) is + # a subtype of def (int | str) -> int | str. + combined = union_function_signatures(left.items) + if combined is not None and self._is_subtype(combined, right): + return True return False elif isinstance(right, Overloaded): if left == self.right: @@ -2147,6 +2154,98 @@ def report(*args: Any) -> None: return cast(NormalizedCallableType, applied) +def union_function_signatures(callables: list[CallableType]) -> CallableType | None: + """Combine a list of functions by taking the union of all the arguments and return types.""" + if len(callables) == 1: + return callables[0] + + # Note: we are assuming here that if a user uses some TypeVar 'T' in + # two different functions, they meant for that TypeVar to mean the + # same thing. + # + # This function will make sure that all instances of that TypeVar 'T' + # refer to the same underlying TypeVarType objects to simplify the union-ing + # logic below. + # + # (If the user did *not* mean for 'T' to be consistently bound to the + # same type in their overloads, well, their code is probably too + # confusing and ought to be re-written anyway.) + callables, variables = merge_typevars_in_callables_by_name(callables) + + new_args: list[list[Type]] = [[] for _ in callables[0].arg_types] + new_kinds = list(callables[0].arg_kinds) + new_returns: list[Type] = [] + + for target in callables: + # TODO: Enhance the merging logic to handle a wider variety of signatures. + if len(new_kinds) != len(target.arg_kinds): + return None + for i, (new_kind, target_kind) in enumerate(zip(new_kinds, target.arg_kinds)): + if new_kind == target_kind: + continue + if new_kind.is_positional() and target_kind.is_positional(): + new_kinds[i] = ARG_POS + else: + return None + + for i, arg in enumerate(target.arg_types): + new_args[i].append(arg) + new_returns.append(target.ret_type) + + return callables[0].copy_modified( + arg_types=[mypy.typeops.make_simplified_union(args) for args in new_args], + arg_kinds=new_kinds, + ret_type=mypy.typeops.make_simplified_union(new_returns), + variables=variables, + implicit=True, + ) + + +def merge_typevars_in_callables_by_name( + callables: Sequence[CallableType], +) -> tuple[list[CallableType], list[TypeVarType]]: + """Takes all the typevars present in the callables and 'combines' the ones with the same name. + + For example, suppose we have two callables with signatures "f(x: T, y: S) -> T" and + "f(x: List[Tuple[T, S]]) -> Tuple[T, S]". Both callables use typevars named "T" and + "S", but we treat them as distinct, unrelated typevars. (E.g. they could both have + distinct ids.) + + If we pass in both callables into this function, it returns a list containing two + new callables that are identical in signature, but use the same underlying TypeVarType + for T and S. + + This is useful if we want to take the output lists and "merge" them into one callable + in some way -- for example, when unioning together overloads. + + Returns both the new list of callables and a list of all distinct TypeVarType objects used. + """ + output: list[CallableType] = [] + unique_typevars: dict[str, TypeVarType] = {} + variables: list[TypeVarType] = [] + + for target in callables: + if target.is_generic(): + target = freshen_function_type_vars(target) + + rename = {} # Mapping TypeVarId -> TypeVar + for tv in target.variables: + name = tv.fullname + if name not in unique_typevars: + # TODO: support ParamSpecType and TypeVarTuple. + if isinstance(tv, (ParamSpecType, TypeVarTupleType)): + continue + assert isinstance(tv, TypeVarType) + unique_typevars[name] = tv + variables.append(tv) + rename[tv.id] = unique_typevars[name] + + target = expand_type(target, rename) + output.append(target) + + return output, variables + + def try_restrict_literal_union(t: UnionType, s: Type) -> list[Type] | None: """Return the items of t, excluding any occurrence of s, if and only if - t only contains simple literals diff --git a/test-data/unit/check-overloading.test b/test-data/unit/check-overloading.test index 65e861057a9e6..54d0ea264e707 100644 --- a/test-data/unit/check-overloading.test +++ b/test-data/unit/check-overloading.test @@ -6856,3 +6856,48 @@ if isinstance(headers, dict): reveal_type(headers) # N: Revealed type is "__main__.Headers | typing.Iterable[tuple[builtins.bytes, builtins.bytes]]" [builtins fixtures/isinstancelist.pyi] + +[case testOverloadUnionCallableSubtype] +from typing import Callable, Union, overload + +def f(fn: Callable[[Union[int, str]], Union[int, str]]) -> None: pass +def f1(fn: Callable[[Union[int, str, bytes]], Union[int, str]]) -> None: pass +def f2(fn: Callable[[Union[int, str]], int]) -> None: pass + +@overload +def g(x: int) -> int: ... +@overload +def g(x: str) -> str: ... +def g(x): ... + +f(g) +f1(g) # E: Argument 1 to "f1" has incompatible type overloaded function; expected "Callable[[int | str | bytes], int | str]" +f2(g) # E: Argument 1 to "f2" has incompatible type overloaded function; expected "Callable[[int | str], int]" + +[case testOverloadOverridesUnionCallable] +from typing import overload, Union + +class A: + def f(self, x: Union[int, str]) -> None: ... + +class B(A): + @overload + def f(self, x: int) -> None: ... + @overload + def f(self, x: str) -> None: ... + def f(self, x: Union[int, str]) -> None: ... + +[case testOverloadOverridesUnionCallableGeneric] +from typing import overload, Optional, TypeVar + +T = TypeVar("T") + +class A: + def f(self, x: Optional[T]) -> Optional[T]: ... + +class B(A): + @overload + def f(self, x: None) -> None: ... + @overload + def f(self, x: T) -> T: ... + def f(self, x: Optional[T]) -> Optional[T]: ... From 2ba79be0bd6a0a5d9f54459765b02f2d1eadf06a Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sat, 19 Sep 2026 23:45:39 +0100 Subject: [PATCH 2/2] Fix infinite recursion by removing seemingly redundant freshen call --- mypy/subtypes.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/mypy/subtypes.py b/mypy/subtypes.py index 0b7a3fcd768cc..eab88e5b4fdf2 100644 --- a/mypy/subtypes.py +++ b/mypy/subtypes.py @@ -61,6 +61,7 @@ TypedDictType, TypeOfAny, TypeType, + TypeVarLikeType, TypeVarTupleType, TypeVarType, TypeVisitor, @@ -2203,7 +2204,7 @@ def union_function_signatures(callables: list[CallableType]) -> CallableType | N def merge_typevars_in_callables_by_name( callables: Sequence[CallableType], -) -> tuple[list[CallableType], list[TypeVarType]]: +) -> tuple[list[CallableType], list[TypeVarLikeType]]: """Takes all the typevars present in the callables and 'combines' the ones with the same name. For example, suppose we have two callables with signatures "f(x: T, y: S) -> T" and @@ -2221,21 +2222,15 @@ def merge_typevars_in_callables_by_name( Returns both the new list of callables and a list of all distinct TypeVarType objects used. """ output: list[CallableType] = [] - unique_typevars: dict[str, TypeVarType] = {} - variables: list[TypeVarType] = [] + unique_typevars: dict[str, TypeVarLikeType] = {} + variables: list[TypeVarLikeType] = [] for target in callables: if target.is_generic(): - target = freshen_function_type_vars(target) - rename = {} # Mapping TypeVarId -> TypeVar for tv in target.variables: name = tv.fullname if name not in unique_typevars: - # TODO: support ParamSpecType and TypeVarTuple. - if isinstance(tv, (ParamSpecType, TypeVarTupleType)): - continue - assert isinstance(tv, TypeVarType) unique_typevars[name] = tv variables.append(tv) rename[tv.id] = unique_typevars[name]