From 8e8dc222721e49460aae861af0ec3d1c1702ff47 Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Fri, 3 Apr 2026 19:48:38 -0700 Subject: [PATCH 1/2] . --- mypy/erasetype.py | 4 +++- mypy/meet.py | 2 +- mypy/subtypes.py | 5 +++++ mypy/test/testtypes.py | 2 +- test-data/unit/check-abstract.test | 21 +++++++++++++++++++++ test-data/unit/check-functools.test | 3 ++- test-data/unit/check-optional.test | 16 ++++++++++++++++ 7 files changed, 49 insertions(+), 4 deletions(-) diff --git a/mypy/erasetype.py b/mypy/erasetype.py index cb8d66f292dd3..def3e5f5d7879 100644 --- a/mypy/erasetype.py +++ b/mypy/erasetype.py @@ -102,11 +102,13 @@ def visit_unpack_type(self, t: UnpackType) -> ProperType: def visit_callable_type(self, t: CallableType) -> ProperType: # We must preserve the fallback type for overload resolution to work. any_type = AnyType(TypeOfAny.special_form) + # If we're a type object, make sure we continue to be a valid type object + ret_type = t.ret_type if t.is_type_obj() else any_type return CallableType( arg_types=[any_type, any_type], arg_kinds=[ARG_STAR, ARG_STAR2], arg_names=[None, None], - ret_type=any_type, + ret_type=ret_type, fallback=t.fallback, is_ellipsis_args=True, implicit=True, diff --git a/mypy/meet.py b/mypy/meet.py index ee32f239df8c3..470c03490d480 100644 --- a/mypy/meet.py +++ b/mypy/meet.py @@ -657,7 +657,7 @@ def _type_object_overlap(left: Type, right: Type) -> bool: def is_overlapping_erased_types( left: Type, right: Type, *, ignore_promotions: bool = False ) -> bool: - """The same as 'is_overlapping_erased_types', except the types are erased first.""" + """The same as 'is_overlapping_types', except the types are erased first.""" return is_overlapping_types( erase_type(left), erase_type(right), ignore_promotions=ignore_promotions ) diff --git a/mypy/subtypes.py b/mypy/subtypes.py index 66d7a95eb4252..c5ef0b6636ca7 100644 --- a/mypy/subtypes.py +++ b/mypy/subtypes.py @@ -1696,6 +1696,11 @@ def g(x: int) -> int: ... if right.is_type_obj() and not left.is_type_obj() and not allow_partial_overlap: return False + if left.is_type_obj(): + left_type_obj = left.type_object() + if (left_type_obj.is_protocol or left_type_obj.is_abstract) and not right.is_type_obj(): + return False + # A callable L is a subtype of a generic callable R if L is a # subtype of every type obtained from R by substituting types for # the variables of R. We can check this by simply leaving the diff --git a/mypy/test/testtypes.py b/mypy/test/testtypes.py index 6562f541d73bc..0e3635fad5a7b 100644 --- a/mypy/test/testtypes.py +++ b/mypy/test/testtypes.py @@ -318,7 +318,7 @@ def test_erase_with_type_object(self) -> None: arg_types=[self.fx.anyt, self.fx.anyt], arg_kinds=[ARG_STAR, ARG_STAR2], arg_names=[None, None], - ret_type=self.fx.anyt, + ret_type=self.fx.b, fallback=self.fx.type_type, ), ) diff --git a/test-data/unit/check-abstract.test b/test-data/unit/check-abstract.test index 7507a31d115a9..4de9f1ede3973 100644 --- a/test-data/unit/check-abstract.test +++ b/test-data/unit/check-abstract.test @@ -1688,3 +1688,24 @@ from typing import TYPE_CHECKING class C: if TYPE_CHECKING: def dynamic(self) -> int: ... # OK + +[case testAbstractCallableSubtyping] +import abc +from typing import Callable, Protocol + +class Proto(Protocol): + def meth(self): ... + +def foo(t: Callable[..., Proto]): + t() + +foo(Proto) # E: Argument 1 to "foo" has incompatible type "Type[Proto]"; expected "Callable[..., Proto]" + +class Abstract(abc.ABC): + @abc.abstractmethod + def meth(self): ... + +def bar(t: Callable[..., Abstract]): + t() + +bar(Abstract) # E: Argument 1 to "bar" has incompatible type "Type[Abstract]"; expected "Callable[..., Abstract]" diff --git a/test-data/unit/check-functools.test b/test-data/unit/check-functools.test index 77070d61a013c..347e4d6d562d3 100644 --- a/test-data/unit/check-functools.test +++ b/test-data/unit/check-functools.test @@ -594,7 +594,8 @@ def f1(cls: type[A]) -> None: def f2() -> None: A() # E: Cannot instantiate abstract class "A" with abstract attribute "method" - partial_cls = partial(A) # E: Cannot instantiate abstract class "A" with abstract attribute "method" + partial_cls = partial(A) # E: Cannot instantiate abstract class "A" with abstract attribute "method" \ + # E: Argument 1 to "partial" has incompatible type "Type[A]"; expected "Callable[..., A]" partial_cls() # E: Cannot instantiate abstract class "A" with abstract attribute "method" [builtins fixtures/tuple.pyi] diff --git a/test-data/unit/check-optional.test b/test-data/unit/check-optional.test index 2ccb571792ac0..051654182c567 100644 --- a/test-data/unit/check-optional.test +++ b/test-data/unit/check-optional.test @@ -1355,3 +1355,19 @@ def f(x: object) -> None: with C(): pass [builtins fixtures/tuple.pyi] + +[case testRefineAwayNoneCallbackProtocol] +# Regression test for issue encountered in https://github.com/python/mypy/pull/18347#issuecomment-2564062070 +from __future__ import annotations +from typing import Protocol + +class CP(Protocol): + def __call__(self, parameters: str) -> str: ... + +class NotSet: ... + +class Task: + def with_opt(self, trn: CP | type[NotSet] | None): + if trn is not NotSet: + reveal_type(trn) # N: Revealed type is "Union[__main__.CP, Type[__main__.NotSet], None]" +[builtins fixtures/tuple.pyi] From ea2595a8612f73636bb6c3406d2267b807043325 Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Fri, 3 Apr 2026 21:19:51 -0700 Subject: [PATCH 2/2] update --- test-data/unit/check-abstract.test | 4 ++-- test-data/unit/check-functools.test | 3 ++- test-data/unit/check-optional.test | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/test-data/unit/check-abstract.test b/test-data/unit/check-abstract.test index 4de9f1ede3973..b9feb9ff68ac9 100644 --- a/test-data/unit/check-abstract.test +++ b/test-data/unit/check-abstract.test @@ -1699,7 +1699,7 @@ class Proto(Protocol): def foo(t: Callable[..., Proto]): t() -foo(Proto) # E: Argument 1 to "foo" has incompatible type "Type[Proto]"; expected "Callable[..., Proto]" +foo(Proto) # E: Argument 1 to "foo" has incompatible type "type[Proto]"; expected "Callable[..., Proto]" class Abstract(abc.ABC): @abc.abstractmethod @@ -1708,4 +1708,4 @@ class Abstract(abc.ABC): def bar(t: Callable[..., Abstract]): t() -bar(Abstract) # E: Argument 1 to "bar" has incompatible type "Type[Abstract]"; expected "Callable[..., Abstract]" +bar(Abstract) # E: Argument 1 to "bar" has incompatible type "type[Abstract]"; expected "Callable[..., Abstract]" diff --git a/test-data/unit/check-functools.test b/test-data/unit/check-functools.test index 347e4d6d562d3..fca1e65e984a7 100644 --- a/test-data/unit/check-functools.test +++ b/test-data/unit/check-functools.test @@ -595,8 +595,9 @@ def f1(cls: type[A]) -> None: def f2() -> None: A() # E: Cannot instantiate abstract class "A" with abstract attribute "method" partial_cls = partial(A) # E: Cannot instantiate abstract class "A" with abstract attribute "method" \ - # E: Argument 1 to "partial" has incompatible type "Type[A]"; expected "Callable[..., A]" + # E: Argument 1 to "partial" has incompatible type "type[A]"; expected "Callable[..., A]" partial_cls() # E: Cannot instantiate abstract class "A" with abstract attribute "method" + [builtins fixtures/tuple.pyi] [case testFunctoolsPartialSelfType] diff --git a/test-data/unit/check-optional.test b/test-data/unit/check-optional.test index 051654182c567..eeb324fbf3ffb 100644 --- a/test-data/unit/check-optional.test +++ b/test-data/unit/check-optional.test @@ -1369,5 +1369,5 @@ class NotSet: ... class Task: def with_opt(self, trn: CP | type[NotSet] | None): if trn is not NotSet: - reveal_type(trn) # N: Revealed type is "Union[__main__.CP, Type[__main__.NotSet], None]" + reveal_type(trn) # N: Revealed type is "__main__.CP | type[__main__.NotSet] | None" [builtins fixtures/tuple.pyi]