From 0332484ba82821653db7913955804448419906ca Mon Sep 17 00:00:00 2001 From: bzoracler <50305397+bzoracler@users.noreply.github.com> Date: Thu, 23 Oct 2025 13:42:30 +1300 Subject: [PATCH 01/19] feat: report `@deprecated()` on non-overloaded class constructors --- mypy/checkexpr.py | 8 ++++++ test-data/unit/check-deprecated.test | 43 ++++++++++++++++++++++++---- 2 files changed, 46 insertions(+), 5 deletions(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 3eb54579a050..a0c2283ebf59 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -1664,6 +1664,14 @@ def check_callable_call( See the docstring of check_call for more information. """ + # Check implicit calls to deprecated class constructors. + # Only the non-overload case is handled here. Overloaded constructors are handled + # separately during overload resolution. `callable_node` is `None` for an overload + # item so deprecation checks are not duplicated. + if isinstance(callable_node, RefExpr) and isinstance(callable_node.node, TypeInfo): + self.chk.check_deprecated(callable_node.node.get_method("__new__"), context) + self.chk.check_deprecated(callable_node.node.get_method("__init__"), context) + # Always unpack **kwargs before checking a call. callee = callee.with_unpacked_kwargs().with_normalized_var_args() if callable_name is None and callee.name: diff --git a/test-data/unit/check-deprecated.test b/test-data/unit/check-deprecated.test index 607e9d767956..98d0324faf10 100644 --- a/test-data/unit/check-deprecated.test +++ b/test-data/unit/check-deprecated.test @@ -315,18 +315,51 @@ class E: ... [builtins fixtures/tuple.pyi] -[case testDeprecatedClassInitMethod] +[case testDeprecatedClassConstructor] # flags: --enable-error-code=deprecated from typing_extensions import deprecated -@deprecated("use C2 instead") class C: + @deprecated("call `make_c()` instead") def __init__(self) -> None: ... + @classmethod + def make_c(cls) -> C: ... -c: C # E: class __main__.C is deprecated: use C2 instead -C() # E: class __main__.C is deprecated: use C2 instead -C.__init__(c) # E: class __main__.C is deprecated: use C2 instead +C() # E: function __main__.C.__init__ is deprecated: call `make_c()` instead + +class D: + @deprecated("call `make_d()` instead") + def __new__(cls) -> D: ... + @classmethod + def make_d(cls) -> D: ... + +D() # E: function __main__.D.__new__ is deprecated: call `make_d()` instead + +[builtins fixtures/tuple.pyi] + + +[case testDeprecatedSuperClassConstructor] +# flags: --enable-error-code=deprecated + +from typing_extensions import deprecated, Self + +class A: + @deprecated("call `self.initialise()` instead") + def __init__(self) -> None: ... + def initialise(self) -> None: ... + +class B(A): + def __init__(self) -> None: + super().__init__() # E: function __main__.A.__init__ is deprecated: call `self.initialise()` instead + +class C: + @deprecated("call `object.__new__(cls)` instead") + def __new__(cls) -> Self: ... + +class D(C): + def __new__(cls) -> Self: + return super().__new__(cls) # E: function __main__.C.__new__ is deprecated: call `object.__new__(cls)` instead [builtins fixtures/tuple.pyi] From d497f37d0119064db50628fbb67aeaa94eb834b8 Mon Sep 17 00:00:00 2001 From: bzoracler <50305397+bzoracler@users.noreply.github.com> Date: Sun, 26 Oct 2025 22:36:30 +1300 Subject: [PATCH 02/19] Add simple subclass tests --- test-data/unit/check-deprecated.test | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/test-data/unit/check-deprecated.test b/test-data/unit/check-deprecated.test index 98d0324faf10..384b0bc27d2c 100644 --- a/test-data/unit/check-deprecated.test +++ b/test-data/unit/check-deprecated.test @@ -326,7 +326,10 @@ class C: @classmethod def make_c(cls) -> C: ... +class C2(C): ... + C() # E: function __main__.C.__init__ is deprecated: call `make_c()` instead +C2() # E: function.__main__.C.__init__ is deprecated: call `make_c()` instead class D: @deprecated("call `make_d()` instead") @@ -334,7 +337,10 @@ class D: @classmethod def make_d(cls) -> D: ... +class D2(D): ... + D() # E: function __main__.D.__new__ is deprecated: call `make_d()` instead +D2() # E: function __main__.D.__new__ is deprecated: call `make_d()` instead [builtins fixtures/tuple.pyi] From 53622b1413b7623ec57fd2d5dbf2eed4f5cc8726 Mon Sep 17 00:00:00 2001 From: bzoracler <50305397+bzoracler@users.noreply.github.com> Date: Sun, 26 Oct 2025 23:04:31 +1300 Subject: [PATCH 03/19] fix error message typo --- test-data/unit/check-deprecated.test | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-data/unit/check-deprecated.test b/test-data/unit/check-deprecated.test index 384b0bc27d2c..c32d8a70a5b6 100644 --- a/test-data/unit/check-deprecated.test +++ b/test-data/unit/check-deprecated.test @@ -329,7 +329,7 @@ class C: class C2(C): ... C() # E: function __main__.C.__init__ is deprecated: call `make_c()` instead -C2() # E: function.__main__.C.__init__ is deprecated: call `make_c()` instead +C2() # E: function __main__.C.__init__ is deprecated: call `make_c()` instead class D: @deprecated("call `make_d()` instead") From a473e52aeef05115c1b92ce3404417a07e22c9ae Mon Sep 17 00:00:00 2001 From: bzoracler Date: Tue, 28 Oct 2025 08:46:24 +1300 Subject: [PATCH 04/19] Save an unnecessary instance check --- mypy/checkexpr.py | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index a0c2283ebf59..ee0d7ed88761 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -1664,14 +1664,6 @@ def check_callable_call( See the docstring of check_call for more information. """ - # Check implicit calls to deprecated class constructors. - # Only the non-overload case is handled here. Overloaded constructors are handled - # separately during overload resolution. `callable_node` is `None` for an overload - # item so deprecation checks are not duplicated. - if isinstance(callable_node, RefExpr) and isinstance(callable_node.node, TypeInfo): - self.chk.check_deprecated(callable_node.node.get_method("__new__"), context) - self.chk.check_deprecated(callable_node.node.get_method("__init__"), context) - # Always unpack **kwargs before checking a call. callee = callee.with_unpacked_kwargs().with_normalized_var_args() if callable_name is None and callee.name: @@ -1679,9 +1671,21 @@ def check_callable_call( ret_type = get_proper_type(callee.ret_type) if callee.is_type_obj() and isinstance(ret_type, Instance): callable_name = ret_type.type.fullname - if isinstance(callable_node, RefExpr) and callable_node.fullname in ENUM_BASES: - # An Enum() call that failed SemanticAnalyzerPass2.check_enum_call(). - return callee.ret_type, callee + if isinstance(callable_node, RefExpr): + # Check implicit calls to deprecated class constructors. + # Only the non-overload case is handled here. Overloaded constructors are handled + # separately during overload resolution. `callable_node` is `None` for an overload + # item so deprecation checks are not duplicated. + callable_info: TypeInfo | None = None + if isinstance(callable_node.node, TypeInfo): + callable_info = callable_node.node + if callable_info is not None: + self.chk.check_deprecated(callable_node.node.get_method("__new__"), context) + self.chk.check_deprecated(callable_node.node.get_method("__init__"), context) + + if callable_node.fullname in ENUM_BASES: + # An Enum() call that failed SemanticAnalyzerPass2.check_enum_call(). + return callee.ret_type, callee if ( callee.is_type_obj() From 8f08c60d9146857a2e7d7e91491bedfde218c2f3 Mon Sep 17 00:00:00 2001 From: bzoracler Date: Tue, 28 Oct 2025 08:47:02 +1300 Subject: [PATCH 05/19] Use the constructor definition chosen by mypy rather than walking 2 MROs --- mypy/checkexpr.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index ee0d7ed88761..3cc08ef7b97c 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -1680,8 +1680,7 @@ def check_callable_call( if isinstance(callable_node.node, TypeInfo): callable_info = callable_node.node if callable_info is not None: - self.chk.check_deprecated(callable_node.node.get_method("__new__"), context) - self.chk.check_deprecated(callable_node.node.get_method("__init__"), context) + self.chk.check_deprecated(callee.definition, context) if callable_node.fullname in ENUM_BASES: # An Enum() call that failed SemanticAnalyzerPass2.check_enum_call(). From b0ffd6b0acdb9d38e539093941a2d8c957e9fe8a Mon Sep 17 00:00:00 2001 From: bzoracler Date: Tue, 28 Oct 2025 08:51:55 +1300 Subject: [PATCH 06/19] Warn deprecated constructor calls from old-style type aliases --- mypy/checkexpr.py | 6 ++++++ test-data/unit/check-deprecated.test | 29 ++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 3cc08ef7b97c..cffee9cf1b0e 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -1679,6 +1679,12 @@ def check_callable_call( callable_info: TypeInfo | None = None if isinstance(callable_node.node, TypeInfo): callable_info = callable_node.node + elif ( + isinstance(callable_node.node, TypeAlias) + and isinstance(callable_node.node.target, Instance) + and isinstance(callable_node.node.target.type, TypeInfo) + ): + callable_info = callable_node.node.target.type if callable_info is not None: self.chk.check_deprecated(callee.definition, context) diff --git a/test-data/unit/check-deprecated.test b/test-data/unit/check-deprecated.test index c32d8a70a5b6..8381fa25e2cd 100644 --- a/test-data/unit/check-deprecated.test +++ b/test-data/unit/check-deprecated.test @@ -370,6 +370,35 @@ class D(C): [builtins fixtures/tuple.pyi] +[case testDeprecatedClassConstructorCalledFromTypeAlias] +# flags: --enable-error-code=deprecated + +from typing_extensions import deprecated, TypeAlias + +class A: + @deprecated("do not use") + def __init__(self) -> None: ... + +class B(A): ... + +A_alias = A +A_explicit_alias: TypeAlias = A +B_alias = B +B_explicit_alias: TypeAlias = B + +A_alias() # E: function __main__.A.__init__ is deprecated: do not use +A_explicit_alias() # E: function __main__.A.__init__ is deprecated: do not use +B_alias() # E: function __main__.A.__init__ is deprecated: do not use +B_explicit_alias() # E: function __main__.A.__init__ is deprecated: do not use + +A_alias +A_explicit_alias +B_alias +B_explicit_alias + +[builtins fixtures/tuple.pyi] + + [case testDeprecatedSpecialMethods] # flags: --enable-error-code=deprecated From c954f78ba4cb219e160feae22e0242b52c538baa Mon Sep 17 00:00:00 2001 From: bzoracler Date: Tue, 28 Oct 2025 08:59:26 +1300 Subject: [PATCH 07/19] fix unexpanded type check error --- mypy/checkexpr.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index cffee9cf1b0e..7b4fbdcd0820 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -1679,12 +1679,10 @@ def check_callable_call( callable_info: TypeInfo | None = None if isinstance(callable_node.node, TypeInfo): callable_info = callable_node.node - elif ( - isinstance(callable_node.node, TypeAlias) - and isinstance(callable_node.node.target, Instance) - and isinstance(callable_node.node.target.type, TypeInfo) - ): - callable_info = callable_node.node.target.type + elif isinstance(callable_node.node, TypeAlias): + alias_target = get_proper_type(callable_node.node.target) + if isinstance(alias_target, Instance) and isinstance(alias_target.type, TypeInfo): + callable_info = callable_node.node.target.type if callable_info is not None: self.chk.check_deprecated(callee.definition, context) From 00897abdb5021129db91a6c7b91d3c2d850823e0 Mon Sep 17 00:00:00 2001 From: bzoracler Date: Tue, 28 Oct 2025 09:00:04 +1300 Subject: [PATCH 08/19] fix unexpanded type check error --- mypy/checkexpr.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 7b4fbdcd0820..c0edb7780cfd 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -1682,7 +1682,7 @@ def check_callable_call( elif isinstance(callable_node.node, TypeAlias): alias_target = get_proper_type(callable_node.node.target) if isinstance(alias_target, Instance) and isinstance(alias_target.type, TypeInfo): - callable_info = callable_node.node.target.type + callable_info = alias_target.type if callable_info is not None: self.chk.check_deprecated(callee.definition, context) From 3b7b986f0736e75d7c10d5d53def337ea0d9baaf Mon Sep 17 00:00:00 2001 From: bzoracler Date: Thu, 30 Oct 2025 17:21:15 +1300 Subject: [PATCH 09/19] augment basic deprecation tests with override checks --- test-data/unit/check-deprecated.test | 31 +++++++++++++++++++++------- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/test-data/unit/check-deprecated.test b/test-data/unit/check-deprecated.test index 8381fa25e2cd..38641263017f 100644 --- a/test-data/unit/check-deprecated.test +++ b/test-data/unit/check-deprecated.test @@ -328,8 +328,12 @@ class C: class C2(C): ... +class C3(C): + def __init__(self) -> None: ... + C() # E: function __main__.C.__init__ is deprecated: call `make_c()` instead C2() # E: function __main__.C.__init__ is deprecated: call `make_c()` instead +C3() class D: @deprecated("call `make_d()` instead") @@ -339,8 +343,12 @@ class D: class D2(D): ... +class D3(D): + def __new__(cls) -> D3: ... + D() # E: function __main__.D.__new__ is deprecated: call `make_d()` instead D2() # E: function __main__.D.__new__ is deprecated: call `make_d()` instead +D3() [builtins fixtures/tuple.pyi] @@ -379,22 +387,31 @@ class A: @deprecated("do not use") def __init__(self) -> None: ... -class B(A): ... +class B1(A): ... + +class B2(A): + def __init__(self) -> None: ... A_alias = A A_explicit_alias: TypeAlias = A -B_alias = B -B_explicit_alias: TypeAlias = B +B1_alias = B1 +B1_explicit_alias: TypeAlias = B1 +B2_alias = B2 +B2_explicit_alias: TypeAlias = B2 A_alias() # E: function __main__.A.__init__ is deprecated: do not use A_explicit_alias() # E: function __main__.A.__init__ is deprecated: do not use -B_alias() # E: function __main__.A.__init__ is deprecated: do not use -B_explicit_alias() # E: function __main__.A.__init__ is deprecated: do not use +B1_alias() # E: function __main__.A.__init__ is deprecated: do not use +B1_explicit_alias() # E: function __main__.A.__init__ is deprecated: do not use +B2_alias() +B2_explicit_alias() A_alias A_explicit_alias -B_alias -B_explicit_alias +B1_alias +B1_explicit_alias +B2_alias +B2_explicit_alias [builtins fixtures/tuple.pyi] From 4b42ea6ade0e9f58cc7378802ba75f8ea39ce0f6 Mon Sep 17 00:00:00 2001 From: bzoracler Date: Thu, 30 Oct 2025 17:25:09 +1300 Subject: [PATCH 10/19] add specific tests for deprecated overloaded class constructors --- test-data/unit/check-deprecated.test | 79 ++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) diff --git a/test-data/unit/check-deprecated.test b/test-data/unit/check-deprecated.test index 38641263017f..b1e8617b4f50 100644 --- a/test-data/unit/check-deprecated.test +++ b/test-data/unit/check-deprecated.test @@ -666,6 +666,85 @@ a += "x" # E: function __main__.A.__iadd__ is deprecated: no A += Any [builtins fixtures/tuple.pyi] +[case testDeprecatedOverloadedClassConstructor] +# flags: --enable-error-code=deprecated --disable-error-code=no-overload-impl + +from typing_extensions import TypeAlias, deprecated, overload + +class A: + @overload + @deprecated("do not pass int") + def __new__(cls, arg: int) -> A: ... + @overload + def __new__(cls, arg: str) -> A: ... + +class B: + @overload + @deprecated("do not pass int") + def __init__(self, arg: int) -> None: ... + @overload + def __init__(self, arg: str) -> None: ... + +A_alias: TypeAlias = A +B_alias: TypeAlias = B + +A(1) # E: overload def (cls: type[__main__.A], arg: builtins.int) -> __main__.A of function __main__.A.__new__ is deprecated: do not pass int +A_alias(1) # E: overload def (cls: type[__main__.A], arg: builtins.int) -> __main__.A of function __main__.A.__new__ is deprecated: do not pass int +A("") +A_alias("") +class A_child(A): + def __new__(cls) -> A_child: + super().__new__(cls, 1) # E: overload def (cls: type[__main__.A], arg: builtins.int) -> __main__.A of function __main__.A.__new__ is deprecated: do not pass int + super().__new__(cls, "") + return object.__new__(cls) + +B(1) # E: overload def (self: __main__.B, arg: builtins.int) of function __main__.B.__init__ is deprecated: do not pass int +B_alias(1) # E: overload def (self: __main__.B, arg: builtins.int) of function __main__.B.__init__ is deprecated: do not pass int +B("") +B_alias("") +class B_child(B): + def __init__(self) -> None: + super().__init__(1) # E: overload def (self: __main__.B, arg: builtins.int) of function __main__.B.__init__ is deprecated: do not pass int + super().__init__("") + +[builtins fixtures/tuple.pyi] + + +[case testDeprecatedOverloadedClassConstructorDecoratingOverloadedFunction] + +# flags: --enable-error-code=deprecated --disable-error-code=no-overload-impl + +from typing import Callable, Generic, TypeVar +from typing_extensions import deprecated, overload + +F = TypeVar("F", bound=Callable[..., object]) + +class decorator(Generic[F]): + __call__: F + + @overload + @deprecated("decorated function must take arguments") + def __init__(self: decorator[Callable[[], None]], f: Callable[[], None]) -> None: ... + @overload + def __init__(self, f: F) -> None: ... + +@overload +@decorator +def f(a: str) -> None: ... +@overload # E: overload def (self: __main__.decorator[def ()], f: def ()) of function __main__.decorator.__init__ is deprecated: decorated function must take arguments +@decorator +def f() -> None: ... + +f() +f("") +f(1) # E: No overload variant of "f" matches argument type "int" \ + # N: Possible overload variants: \ + # N: def f(a: str) -> None \ + # N: def f() -> None + +[builtins fixtures/tuple.pyi] + + [case testDeprecatedMethod] # flags: --enable-error-code=deprecated From 9e49aa1037edbb953836a72bccbc36bc02403865 Mon Sep 17 00:00:00 2001 From: bzoracler Date: Thu, 30 Oct 2025 17:26:13 +1300 Subject: [PATCH 11/19] add test for accessing methods via `self` and `cls` --- test-data/unit/check-deprecated.test | 34 ++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/test-data/unit/check-deprecated.test b/test-data/unit/check-deprecated.test index b1e8617b4f50..c46a52535804 100644 --- a/test-data/unit/check-deprecated.test +++ b/test-data/unit/check-deprecated.test @@ -780,6 +780,40 @@ C().k() # E: function __main__.C.k is deprecated: use g instead [builtins fixtures/callable.pyi] +[case testDeprecatedMethodAccessedFromMethod] +# flags: --enable-error-code=deprecated + +from typing_extensions import deprecated + +class C: + @deprecated("use g instead") + def f(self) -> None: ... + + def g(self) -> None: ... + + @deprecated("use g instead") + @staticmethod + def h() -> None: ... + + def instance_method(self) -> None: + self.f # E: function __main__.C.f is deprecated: use g instead + self.f() # E: function __main__.C.f is deprecated: use g instead + self.g() + self.h() # E: function __main__.C.h is deprecated: use g instead + t = (self.f, self.f, self.g) # E: function __main__.C.f is deprecated: use g instead + + @classmethod + def class_method(cls) -> None: + c: C + cls.f # E: function __main__.C.f is deprecated: use g instead + cls.f(c) # E: function __main__.C.f is deprecated: use g instead + cls.g(c) + cls.h() # E: function __main__.C.h is deprecated: use g instead + t = (cls.f, cls.f, cls.g) # E: function __main__.C.f is deprecated: use g instead + +[builtins fixtures/callable.pyi] + + [case testDeprecatedClassWithDeprecatedMethod] # flags: --enable-error-code=deprecated From 4be62c6decd4f9144aab10f0f3342fd1dbc67dd9 Mon Sep 17 00:00:00 2001 From: bzoracler Date: Thu, 30 Oct 2025 17:26:45 +1300 Subject: [PATCH 12/19] add tests for accessing class constructor methods directly --- test-data/unit/check-deprecated.test | 65 ++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/test-data/unit/check-deprecated.test b/test-data/unit/check-deprecated.test index c46a52535804..3370f472601a 100644 --- a/test-data/unit/check-deprecated.test +++ b/test-data/unit/check-deprecated.test @@ -814,6 +814,71 @@ class C: [builtins fixtures/callable.pyi] +[case testDeprecatedClassConstructorMethodAccessedFromOutsideMethod] +# flags: --enable-error-code=deprecated + +from typing_extensions import TypeAlias, deprecated + +class C: + @deprecated("use make_c() instead") + def __new__(cls) -> C: ... + + @deprecated("use make_c() instead") + def __init__(self) -> None: ... + + @classmethod + def make_c(cls) -> C: ... + +C_alias: TypeAlias = C +C_type_obj: type[C] +c: C + +C.__new__ # E: function __main__.C.__new__ is deprecated: use make_c() instead +C.__new__(C) # E: function __main__.C.__new__ is deprecated: use make_c() instead +C.__init__ # E: function __main__.C.__init__ is deprecated: use make_c() instead +C.__init__(c) # E: function __main__.C.__init__ is deprecated: use make_c() instead +C_alias.__new__ # E: function __main__.C.__new__ is deprecated: use make_c() instead +C_alias.__new__(C) # E: function __main__.C.__new__ is deprecated: use make_c() instead +C_alias.__init__ # E: function __main__.C.__init__ is deprecated: use make_c() instead +C_alias.__init__(c) # E: function __main__.C.__init__ is deprecated: use make_c() instead +C_type_obj.__new__ # E: function __main__.C.__new__ is deprecated: use make_c() instead +C_type_obj.__new__(C) # E: function __main__.C.__new__ is deprecated: use make_c() instead +C_type_obj.__init__ # E: function __main__.C.__init__ is deprecated: use make_c() instead +C_type_obj.__init__(c) # E: function __main__.C.__init__ is deprecated: use make_c() instead + +(C.__new__, C_alias.__new__, C_type_obj.__new__, C.make_c, c.make_c) # E: function __main__.C.__new__ is deprecated: use make_c() instead +(C.__init__, C_alias.__init__, C_type_obj.__init__, C.make_c, c.make_c) # E: function __main__.C.__init__ is deprecated: use make_c() instead + +[builtins fixtures/tuple.pyi] + + +[case testDeprecatedMethodClassConstructorAccessedFromInsideMethod] +# flags: --enable-error-code=deprecated + +from typing_extensions import deprecated + +class C: + @deprecated("use make_c() instead") + def __new__(cls) -> C: ... + @deprecated("use make_c() instead") + def __init__(self) -> None: ... + + @classmethod + def make_c(cls) -> C: ... + + @classmethod + def class_method(cls) -> None: + self: C + cls.__new__ # E: function __main__.C.__new__ is deprecated: use make_c() instead + cls.__new__(cls) # E: function __main__.C.__new__ is deprecated: use make_c() instead + cls.__init__ # E: function __main__.C.__init__ is deprecated: use make_c() instead + cls.__init__(self) # E: function __main__.C.__init__ is deprecated: use make_c() instead + (cls.__new__, cls.__new__, cls.make_c) # E: function __main__.C.__new__ is deprecated: use make_c() instead + (cls.__init__, cls.__init__, cls.make_c) # E: function __main__.C.__init__ is deprecated: use make_c() instead + +[builtins fixtures/tuple.pyi] + + [case testDeprecatedClassWithDeprecatedMethod] # flags: --enable-error-code=deprecated From 79d0cb0faddcc41960321585c671de7dfe020df1 Mon Sep 17 00:00:00 2001 From: bzoracler Date: Thu, 30 Oct 2025 17:27:59 +1300 Subject: [PATCH 13/19] implement reporting on `type[ClassWithDeprecatedConstructor]()` --- mypy/checkexpr.py | 39 +++++---- test-data/unit/check-deprecated.test | 121 +++++++++++++++++++++++++++ 2 files changed, 142 insertions(+), 18 deletions(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index c0edb7780cfd..1963d7c5ac7b 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -1420,7 +1420,11 @@ def is_generic_decorator_overload_call( return None def handle_decorator_overload_call( - self, callee_type: CallableType, overloaded: Overloaded, ctx: Context + self, + callee_type: CallableType, + overloaded: Overloaded, + ctx: Context, + callee_is_overload_item: bool, ) -> tuple[Type, Type] | None: """Type-check application of a generic callable to an overload. @@ -1432,7 +1436,9 @@ def handle_decorator_overload_call( for item in overloaded.items: arg = TempNode(typ=item) with self.msg.filter_errors() as err: - item_result, inferred_arg = self.check_call(callee_type, [arg], [ARG_POS], ctx) + item_result, inferred_arg = self.check_call( + callee_type, [arg], [ARG_POS], ctx, is_overload_item=callee_is_overload_item + ) if err.has_new_errors(): # This overload doesn't match. continue @@ -1538,6 +1544,7 @@ def check_call( callable_name: str | None = None, object_type: Type | None = None, original_type: Type | None = None, + is_overload_item: bool = False, ) -> tuple[Type, Type]: """Type check a call. @@ -1558,6 +1565,7 @@ def check_call( or None if unavailable (examples: 'builtins.open', 'typing.Mapping.get') object_type: If callable_name refers to a method, the type of the object on which the method is being called + is_overload_item: Whether this check is for an individual overload item """ callee = get_proper_type(callee) @@ -1568,7 +1576,7 @@ def check_call( # Special casing for inline application of generic callables to overloads. # Supporting general case would be tricky, but this should cover 95% of cases. overloaded_result = self.handle_decorator_overload_call( - callee, overloaded, context + callee, overloaded, context, is_overload_item ) if overloaded_result is not None: return overloaded_result @@ -1582,6 +1590,7 @@ def check_call( callable_node, callable_name, object_type, + is_overload_item, ) elif isinstance(callee, Overloaded): return self.check_overload_call( @@ -1659,11 +1668,18 @@ def check_callable_call( callable_node: Expression | None, callable_name: str | None, object_type: Type | None, + is_overload_item: bool = False, ) -> tuple[Type, Type]: """Type check a call that targets a callable value. See the docstring of check_call for more information. """ + # Check implicit calls to deprecated class constructors. + # Only the non-overload case is handled here. Overloaded constructors are handled + # separately during overload resolution. + if (not is_overload_item) and callee.is_type_obj(): + self.chk.warn_deprecated(callee.definition, context) + # Always unpack **kwargs before checking a call. callee = callee.with_unpacked_kwargs().with_normalized_var_args() if callable_name is None and callee.name: @@ -1671,21 +1687,7 @@ def check_callable_call( ret_type = get_proper_type(callee.ret_type) if callee.is_type_obj() and isinstance(ret_type, Instance): callable_name = ret_type.type.fullname - if isinstance(callable_node, RefExpr): - # Check implicit calls to deprecated class constructors. - # Only the non-overload case is handled here. Overloaded constructors are handled - # separately during overload resolution. `callable_node` is `None` for an overload - # item so deprecation checks are not duplicated. - callable_info: TypeInfo | None = None - if isinstance(callable_node.node, TypeInfo): - callable_info = callable_node.node - elif isinstance(callable_node.node, TypeAlias): - alias_target = get_proper_type(callable_node.node.target) - if isinstance(alias_target, Instance) and isinstance(alias_target.type, TypeInfo): - callable_info = alias_target.type - if callable_info is not None: - self.chk.check_deprecated(callee.definition, context) - + if isinstance(callable_node, RefExpr) and (callable_node.fullname in ENUM_BASES): if callable_node.fullname in ENUM_BASES: # An Enum() call that failed SemanticAnalyzerPass2.check_enum_call(). return callee.ret_type, callee @@ -2925,6 +2927,7 @@ def infer_overload_return_type( context=context, callable_name=callable_name, object_type=object_type, + is_overload_item=True, ) is_match = not w.has_new_errors() if is_match: diff --git a/test-data/unit/check-deprecated.test b/test-data/unit/check-deprecated.test index 3370f472601a..6688abea4f43 100644 --- a/test-data/unit/check-deprecated.test +++ b/test-data/unit/check-deprecated.test @@ -416,6 +416,127 @@ B2_explicit_alias [builtins fixtures/tuple.pyi] +[case testDeprecatedClassConstructorCalledFromTypeType] +# flags: --enable-error-code=deprecated + +from typing_extensions import deprecated + +class A: + @deprecated("do not use") + def __init__(self) -> None: ... + +class B1(A): ... + +class B2(A): + def __init__(self) -> None: ... + +def get_class_A() -> type[A]: ... +def get_class_B1() -> type[B1]: ... +def get_class_B2() -> type[B2]: ... + +A_type_obj: type[A] +B1_type_obj: type[B1] +B2_type_obj: type[B2] +A_type_obj() # E: function __main__.A.__init__ is deprecated: do not use +B1_type_obj() # E: function __main__.A.__init__ is deprecated: do not use +B2_type_obj() +get_class_A()() # E: function __main__.A.__init__ is deprecated: do not use +get_class_B1()() # E: function __main__.A.__init__ is deprecated: do not use +get_class_B2()() + +def call_class_A(cls: type[A]) -> None: + cls() # E: function __main__.A.__init__ is deprecated: do not use + +def call_class_B1(cls: type[B1]) -> None: + cls() # E: function __main__.A.__init__ is deprecated: do not use + +def call_class_B2(cls: type[B2]) -> None: + cls() + +[builtins fixtures/tuple.pyi] + + +[case testDeprecatedClassConstructorTypeNarrowing] +# flags: --enable-error-code=deprecated + +from typing import TypeVar +from typing_extensions import TypeAlias, TypeIs, deprecated + +T = TypeVar("T") + +class Dummy: ... + +class A: + @deprecated("do not use") + def __init__(self) -> None: ... + +class B(A): + def __init__(self) -> None: ... + +def maybe() -> bool: ... + +# `builtins.issubclass()` does not type-narrow properly +def is_subclass(c1: type, c2: type[T]) -> TypeIs[type[T]]: ... + +A_alias: TypeAlias = A +A_type_obj: type[A] +B_alias: TypeAlias = B +B_type_obj: type[B] + +maybe_A = A if maybe() else None +maybe_A_alias = A_alias if maybe() else None +maybe_A_type_obj = A_type_obj if maybe() else None +if maybe_A is not None: + maybe_A() # E: function __main__.A.__init__ is deprecated: do not use +else: + maybe_A() # E: "None" not callable +if maybe_A_alias is not None: + maybe_A_alias() # E: function __main__.A.__init__ is deprecated: do not use +else: + maybe_A_alias() # E: "None" not callable +if maybe_A_type_obj is not None: + maybe_A_type_obj() # E: function __main__.A.__init__ is deprecated: do not use +else: + maybe_A_type_obj() # E: "None" not callable + +A_or_Dummy = A if maybe() else Dummy +A_type_obj_or_Dummy = A_type_obj if maybe() else Dummy +if is_subclass(A_or_Dummy, A): + A_or_Dummy() # E: function __main__.A.__init__ is deprecated: do not use +else: + A_or_Dummy() +if is_subclass(A_or_Dummy, Dummy): + A_or_Dummy() +else: + A_or_Dummy() # E: function __main__.A.__init__ is deprecated: do not use +if is_subclass(A_type_obj_or_Dummy, A): + A_type_obj_or_Dummy() # E: function __main__.A.__init__ is deprecated: do not use +else: + A_type_obj_or_Dummy() +if is_subclass(A_type_obj_or_Dummy, Dummy): + A_type_obj_or_Dummy() +else: + A_type_obj_or_Dummy() # E: function __main__.A.__init__ is deprecated: do not use + +A_or_B = A if maybe() else B +A_or_B_alias = A if maybe() else B_alias +A_or_B_type_obj = A if maybe() else B_type_obj +if is_subclass(A_or_B, B): + A_or_B() +else: + A_or_B() # E: function __main__.A.__init__ is deprecated: do not use +if is_subclass(A_or_B_alias, B): + A_or_B_alias() +else: + A_or_B_alias() # E: function __main__.A.__init__ is deprecated: do not use +if is_subclass(A_or_B_type_obj, B): + A_or_B_type_obj() +else: + A_or_B_type_obj() # E: function __main__.A.__init__ is deprecated: do not use + +[builtins fixtures/tuple.pyi] + + [case testDeprecatedSpecialMethods] # flags: --enable-error-code=deprecated From 5302af3ff8dc403b0eb6585649b614c343ea49ae Mon Sep 17 00:00:00 2001 From: bzoracler Date: Thu, 30 Oct 2025 17:28:43 +1300 Subject: [PATCH 14/19] add incremental test for calling deprecated constructors --- test-data/unit/check-incremental.test | 88 +++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) diff --git a/test-data/unit/check-incremental.test b/test-data/unit/check-incremental.test index 94f65a950062..9b0f5302844c 100644 --- a/test-data/unit/check-incremental.test +++ b/test-data/unit/check-incremental.test @@ -7512,3 +7512,91 @@ tmp/impl.py:31: note: Revealed type is "builtins.object" tmp/impl.py:32: note: Revealed type is "Union[builtins.int, builtins.str, lib.Unrelated]" tmp/impl.py:33: note: Revealed type is "builtins.object" tmp/impl.py:34: note: Revealed type is "builtins.object" + + +[case testIncrementalDeprecatedClassConstructorCallExpressions] +# flags: --enable-error-code=deprecated + +# Tests whether adding/removing `@deprecated()` on class constructors correctly update +# when the call is from a class, the class's children, and from type aliases. +# Addition/removal is emulated by setting up 2 parent classes across 3 versions of `mod.py`. +# `Parent1.__init__` has `@deprecated()`: yes/no/yes in v1/v2/v3, respectively. +# `Parent2.__init__` has `@deprecated()`: no/yes/no in v1/v2/v3, respectively. + +from typing_extensions import TypeAlias +import mod + +Parent1_alias: TypeAlias = mod.Parent1 +Parent1_type_obj: type[mod.Parent1] +Parent2_alias: TypeAlias = mod.Parent2 +Parent2_type_obj: type[mod.Parent2] +class Child1_of_Parent1(mod.Parent1): ... +class Child1_of_Parent2(mod.Parent2): ... + +# Emits errors in v1 and v3 +Parent1_alias() +Parent1_type_obj() +Child1_of_Parent1() +class Child2_of_Parent1(mod.Parent1): + def __init__(self) -> None: + super().__init__() + +# Emits errors in v2 only +Parent2_alias() +Parent2_type_obj() +Child1_of_Parent2() +class Child2_of_Parent2(mod.Parent2): + def __init__(self) -> None: + super().__init__() + +# Never emits errors +Child2_of_Parent1() +Child2_of_Parent2() + +[file mod.py] +from typing_extensions import TypeAlias, deprecated + +class Parent1: + @deprecated("v1: do not use Parent1.__init__") + def __init__(self) -> None: ... + +class Parent2: + def __init__(self) -> None: ... + +[file mod.py.2] +from typing_extensions import TypeAlias, deprecated + +class Parent1: + def __init__(self) -> None: ... + +class Parent2: + @deprecated("v2: do not use Parent2.__init__") + def __init__(self) -> None: ... + +[file mod.py.3] +from typing_extensions import TypeAlias, deprecated + +class Parent1: + @deprecated("v3: do not use Parent1.__init__") + def __init__(self) -> None: ... + +class Parent2: + def __init__(self) -> None: ... + +[builtins fixtures/tuple.pyi] +[stale mod] +[out] +main:20: error: function mod.Parent1.__init__ is deprecated: v1: do not use Parent1.__init__ +main:21: error: function mod.Parent1.__init__ is deprecated: v1: do not use Parent1.__init__ +main:22: error: function mod.Parent1.__init__ is deprecated: v1: do not use Parent1.__init__ +main:25: error: function mod.Parent1.__init__ is deprecated: v1: do not use Parent1.__init__ +[out2] +main:28: error: function mod.Parent2.__init__ is deprecated: v2: do not use Parent2.__init__ +main:29: error: function mod.Parent2.__init__ is deprecated: v2: do not use Parent2.__init__ +main:30: error: function mod.Parent2.__init__ is deprecated: v2: do not use Parent2.__init__ +main:33: error: function mod.Parent2.__init__ is deprecated: v2: do not use Parent2.__init__ +[out3] +main:20: error: function mod.Parent1.__init__ is deprecated: v3: do not use Parent1.__init__ +main:21: error: function mod.Parent1.__init__ is deprecated: v3: do not use Parent1.__init__ +main:22: error: function mod.Parent1.__init__ is deprecated: v3: do not use Parent1.__init__ +main:25: error: function mod.Parent1.__init__ is deprecated: v3: do not use Parent1.__init__ From d595948c2f80b62623a301a2f66375580750acfc Mon Sep 17 00:00:00 2001 From: bzoracler Date: Fri, 31 Oct 2025 08:55:27 +1300 Subject: [PATCH 15/19] add tests for unioned type calls to deprecated constructors --- test-data/unit/check-deprecated.test | 62 ++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/test-data/unit/check-deprecated.test b/test-data/unit/check-deprecated.test index 6688abea4f43..4d1870d0029f 100644 --- a/test-data/unit/check-deprecated.test +++ b/test-data/unit/check-deprecated.test @@ -456,6 +456,68 @@ def call_class_B2(cls: type[B2]) -> None: [builtins fixtures/tuple.pyi] +[case testDeprecatedClassConstructorCalledFromUnion] +# flags: --enable-error-code=deprecated + +from typing import Callable, Union +from typing_extensions import TypeAlias, deprecated + +def maybe() -> bool: ... + +def dummy_factory() -> Dummy: ... + +class Dummy: ... + +class A: + @deprecated("do not use A()") + def __init__(self) -> None: ... + +class B1(A): + def __init__(self) -> None: ... + +class B2(A): + @deprecated("do not use B2()") + def __init__(self) -> None: ... + +A_alias: TypeAlias = A +A_type_obj: type[A] +B1_alias: TypeAlias = B1 +B1_type_obj: type[B1] + +maybe_A = A if maybe() else None +maybe_A_alias = A_alias if maybe() else None +maybe_A_type_obj = A_type_obj if maybe() else None +maybe_A() # E: function __main__.A.__init__ is deprecated: do not use A() \ + # E: "None" not callable +maybe_A_alias() # E: function __main__.A.__init__ is deprecated: do not use A() \ + # E: "None" not callable +maybe_A_type_obj() # E: function __main__.A.__init__ is deprecated: do not use A() \ + # E: "None" not callable + +maybe_B1 = B1 if maybe() else None +maybe_B1_alias = B1_alias if maybe() else None +maybe_B1_type_obj = B1_type_obj if maybe() else None +maybe_B1() # E: "None" not callable +maybe_B1_alias() # E: "None" not callable +maybe_B1_type_obj() # E: "None" not callable + +A_or_B1: type[Union[A, B1]] +A_or_B2: type[Union[A, B2]] +A_or_dummy: type[Union[A, Dummy]] +A_or_dummy_factory: Union[type[A], Callable[[], Dummy]] +reveal_type(A_or_B1()) # E: function __main__.A.__init__ is deprecated: do not use A() \ + # N: Revealed type is "__main__.A" +reveal_type(A_or_B2()) # E: function __main__.A.__init__ is deprecated: do not use A() \ + # E: function __main__.B2.__init__ is deprecated: do not use B2() \ + # N: Revealed type is "__main__.A" +reveal_type(A_or_dummy()) # E: function __main__.A.__init__ is deprecated: do not use A() \ + # N: Revealed type is "Union[__main__.A, __main__.Dummy]" +reveal_type(A_or_dummy_factory()) # E: function __main__.A.__init__ is deprecated: do not use A() \ + # N: Revealed type is "Union[__main__.A, __main__.Dummy]" + +[builtins fixtures/tuple.pyi] + + [case testDeprecatedClassConstructorTypeNarrowing] # flags: --enable-error-code=deprecated From c7c580ce95b0e302b969cd9fd71be70271a9be7d Mon Sep 17 00:00:00 2001 From: bzoracler Date: Sun, 2 Nov 2025 07:22:06 +1300 Subject: [PATCH 16/19] fix reversion error --- mypy/checkexpr.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 1963d7c5ac7b..71ddb12977c5 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -1688,8 +1688,8 @@ def check_callable_call( if callee.is_type_obj() and isinstance(ret_type, Instance): callable_name = ret_type.type.fullname if isinstance(callable_node, RefExpr) and (callable_node.fullname in ENUM_BASES): + # An Enum() call that failed SemanticAnalyzerPass2.check_enum_call(). if callable_node.fullname in ENUM_BASES: - # An Enum() call that failed SemanticAnalyzerPass2.check_enum_call(). return callee.ret_type, callee if ( From f02dc8cbee7fa1ae2541895f674919c117d1b61c Mon Sep 17 00:00:00 2001 From: bzoracler Date: Sun, 2 Nov 2025 07:26:38 +1300 Subject: [PATCH 17/19] fix reversion error --- mypy/checkexpr.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 71ddb12977c5..abab430e9740 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -1689,8 +1689,7 @@ def check_callable_call( callable_name = ret_type.type.fullname if isinstance(callable_node, RefExpr) and (callable_node.fullname in ENUM_BASES): # An Enum() call that failed SemanticAnalyzerPass2.check_enum_call(). - if callable_node.fullname in ENUM_BASES: - return callee.ret_type, callee + return callee.ret_type, callee if ( callee.is_type_obj() From 9cfdf7adf8043ae083858a0cc3195c8ca8f84eed Mon Sep 17 00:00:00 2001 From: bzoracler Date: Sun, 2 Nov 2025 07:42:07 +1300 Subject: [PATCH 18/19] Restore original formatting --- mypy/checkexpr.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index abab430e9740..6352d81602ef 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -1687,7 +1687,7 @@ def check_callable_call( ret_type = get_proper_type(callee.ret_type) if callee.is_type_obj() and isinstance(ret_type, Instance): callable_name = ret_type.type.fullname - if isinstance(callable_node, RefExpr) and (callable_node.fullname in ENUM_BASES): + if isinstance(callable_node, RefExpr) and callable_node.fullname in ENUM_BASES: # An Enum() call that failed SemanticAnalyzerPass2.check_enum_call(). return callee.ret_type, callee From 7c0b5e3f331fc885a0ceebab36ac1faa336e6caa Mon Sep 17 00:00:00 2001 From: bzoracler Date: Tue, 4 Nov 2025 10:32:35 +1300 Subject: [PATCH 19/19] Use `int()` instead of custom bool function --- test-data/unit/check-deprecated.test | 32 ++++++++++++---------------- 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/test-data/unit/check-deprecated.test b/test-data/unit/check-deprecated.test index 4d1870d0029f..fe747cc44ba8 100644 --- a/test-data/unit/check-deprecated.test +++ b/test-data/unit/check-deprecated.test @@ -462,8 +462,6 @@ def call_class_B2(cls: type[B2]) -> None: from typing import Callable, Union from typing_extensions import TypeAlias, deprecated -def maybe() -> bool: ... - def dummy_factory() -> Dummy: ... class Dummy: ... @@ -484,9 +482,9 @@ A_type_obj: type[A] B1_alias: TypeAlias = B1 B1_type_obj: type[B1] -maybe_A = A if maybe() else None -maybe_A_alias = A_alias if maybe() else None -maybe_A_type_obj = A_type_obj if maybe() else None +maybe_A = A if int() else None +maybe_A_alias = A_alias if int() else None +maybe_A_type_obj = A_type_obj if int() else None maybe_A() # E: function __main__.A.__init__ is deprecated: do not use A() \ # E: "None" not callable maybe_A_alias() # E: function __main__.A.__init__ is deprecated: do not use A() \ @@ -494,9 +492,9 @@ maybe_A_alias() # E: function __main__.A.__init__ is deprecated: do not use A() maybe_A_type_obj() # E: function __main__.A.__init__ is deprecated: do not use A() \ # E: "None" not callable -maybe_B1 = B1 if maybe() else None -maybe_B1_alias = B1_alias if maybe() else None -maybe_B1_type_obj = B1_type_obj if maybe() else None +maybe_B1 = B1 if int() else None +maybe_B1_alias = B1_alias if int() else None +maybe_B1_type_obj = B1_type_obj if int() else None maybe_B1() # E: "None" not callable maybe_B1_alias() # E: "None" not callable maybe_B1_type_obj() # E: "None" not callable @@ -535,8 +533,6 @@ class A: class B(A): def __init__(self) -> None: ... -def maybe() -> bool: ... - # `builtins.issubclass()` does not type-narrow properly def is_subclass(c1: type, c2: type[T]) -> TypeIs[type[T]]: ... @@ -545,9 +541,9 @@ A_type_obj: type[A] B_alias: TypeAlias = B B_type_obj: type[B] -maybe_A = A if maybe() else None -maybe_A_alias = A_alias if maybe() else None -maybe_A_type_obj = A_type_obj if maybe() else None +maybe_A = A if int() else None +maybe_A_alias = A_alias if int() else None +maybe_A_type_obj = A_type_obj if int() else None if maybe_A is not None: maybe_A() # E: function __main__.A.__init__ is deprecated: do not use else: @@ -561,8 +557,8 @@ if maybe_A_type_obj is not None: else: maybe_A_type_obj() # E: "None" not callable -A_or_Dummy = A if maybe() else Dummy -A_type_obj_or_Dummy = A_type_obj if maybe() else Dummy +A_or_Dummy = A if int() else Dummy +A_type_obj_or_Dummy = A_type_obj if int() else Dummy if is_subclass(A_or_Dummy, A): A_or_Dummy() # E: function __main__.A.__init__ is deprecated: do not use else: @@ -580,9 +576,9 @@ if is_subclass(A_type_obj_or_Dummy, Dummy): else: A_type_obj_or_Dummy() # E: function __main__.A.__init__ is deprecated: do not use -A_or_B = A if maybe() else B -A_or_B_alias = A if maybe() else B_alias -A_or_B_type_obj = A if maybe() else B_type_obj +A_or_B = A if int() else B +A_or_B_alias = A if int() else B_alias +A_or_B_type_obj = A if int() else B_type_obj if is_subclass(A_or_B, B): A_or_B() else: