crates/ty_python_semantic/resources/mdtest/deprecated.md
@deprecated decoratorThe decorator @deprecated("some message") can be applied to functions, methods, overloads, and
classes. Uses of these items should subsequently produce a warning.
from typing_extensions import deprecated
@deprecated("use OtherClass")
def myfunc(x: int): ...
myfunc(1) # error: [deprecated] "use OtherClass"
from typing_extensions import deprecated
@deprecated("use BetterClass")
class MyClass: ...
MyClass() # error: [deprecated] "use BetterClass"
from typing_extensions import deprecated
class MyClass:
@deprecated("use something else")
def afunc(): ...
@deprecated("don't use this!")
def amethod(self): ...
MyClass.afunc() # error: [deprecated] "use something else"
MyClass().amethod() # error: [deprecated] "don't use this!"
An outer @deprecated decorator applies to the function returned by inner decorators.
from collections.abc import Callable
from typing import Any, TypeVar
from typing_extensions import deprecated
R = TypeVar("R")
def replacement() -> str:
return "replacement"
def replace_with(value: R) -> Callable[[Callable[..., Any]], R]:
def decorator(_function: Callable[..., Any]) -> R:
return value
return decorator
@deprecated("use replacement directly")
@replace_with(replacement)
def old() -> None:
pass
old() # error: [deprecated] "use replacement directly"
replacement()
@replace_with(replacement)
@deprecated("discarded by outer replacement")
def replaced_after_deprecation() -> None:
pass
replaced_after_deprecation()
@deprecated("outer deprecation")
@replace_with(replacement)
@deprecated("inner deprecation")
def multiply_deprecated() -> None:
pass
multiply_deprecated() # error: [deprecated] "outer deprecation"
class StaticMethodReplacement:
@staticmethod
@deprecated("use replacement directly")
@replace_with(replacement)
def old() -> None:
pass
StaticMethodReplacement.old() # error: [deprecated] "use replacement directly"
@deprecated can also wrap other callable objects at runtime, but we currently only preserve the
deprecation when an inner decorator returns a function literal.
from collections.abc import Callable
from typing import Any, TypeVar
from typing_extensions import deprecated
R = TypeVar("R")
def replace_with(value: R) -> Callable[[Callable[..., Any]], R]:
def decorator(_function: Callable[..., Any]) -> R:
return value
return decorator
class Replacement:
def __call__(self) -> str:
return "replacement"
@deprecated("use Replacement directly")
@replace_with(Replacement())
def old() -> None:
pass
old() # TODO: error: [deprecated] "use Replacement directly"
The typeshed declaration of the decorator is as follows:
class deprecated:
message: LiteralString
category: type[Warning] | None
stacklevel: int
def __init__(self, message: LiteralString, /, *, category: type[Warning] | None = ..., stacklevel: int = 1) -> None: ...
def __call__(self, arg: _T, /) -> _T: ...
Only the mandatory message string is of interest to static analysis, the other two affect only runtime behavior.
from typing_extensions import deprecated
@deprecated # error: [invalid-argument-type] "LiteralString"
def invalid_deco(): ...
invalid_deco() # error: [missing-argument]
from typing_extensions import deprecated
@deprecated() # error: [missing-argument] "message"
def invalid_deco(): ...
invalid_deco()
The argument is supposed to be a LiteralString, and we can handle simple constant propagations like this:
from typing_extensions import deprecated
x = "message"
@deprecated(x)
def invalid_deco(): ...
invalid_deco() # error: [deprecated] "message"
However sufficiently opaque LiteralStrings we can't resolve, and so we lose the message:
from typing_extensions import deprecated, LiteralString
def opaque() -> LiteralString:
return "message"
@deprecated(opaque())
def valid_deco(): ...
valid_deco() # error: [deprecated]
Fully dynamic strings are technically allowed at runtime, but typeshed mandates that the input is a LiteralString, so we can/should emit a diagnostic for this:
from typing_extensions import deprecated
def opaque() -> str:
return "message"
@deprecated(opaque()) # error: [invalid-argument-type] "LiteralString"
def dubious_deco(): ...
dubious_deco()
Although we have no use for the other arguments, we should still error if they're wrong.
from typing_extensions import deprecated
@deprecated("some message", dsfsdf="whatever") # error: [unknown-argument] "dsfsdf"
def invalid_deco(): ...
invalid_deco()
And we should always handle correct ones fine.
from typing_extensions import deprecated
@deprecated("some message", category=DeprecationWarning, stacklevel=1)
def valid_deco(): ...
valid_deco() # error: [deprecated] "some message"
The category must be a Warning subclass or None.
from typing_extensions import deprecated
@deprecated("some message", category=42) # error: [invalid-argument-type] "type[Warning] | None"
def invalid_category(): ...
@deprecated("some message", category=None)
def no_category(): ...
There are 2 different sources of @deprecated: warnings and typing_extensions. The version in
warnings was added in 3.13, the version in typing_extensions is a compatibility shim.
[environment]
python-version = "3.13"
main.py:
import warnings
import typing_extensions
@warnings.deprecated("nope")
def func1(): ...
@typing_extensions.deprecated("nada")
def func2(): ...
func1() # error: [deprecated] "nope"
func2() # error: [deprecated] "nada"
Importing a deprecated item should produce a warning. Subsequent uses of the deprecated item shouldn't produce a warning.
module.py:
from typing_extensions import deprecated
@deprecated("Use OtherType instead")
class DeprType: ...
@deprecated("Use other_func instead")
def depr_func(): ...
main.py:
# error: [deprecated] "Use OtherType instead"
# error: [deprecated] "Use other_func instead"
from module import DeprType, depr_func
# TODO: these diagnostics ideally shouldn't fire since we warn on the import
DeprType() # error: [deprecated] "Use OtherType instead"
depr_func() # error: [deprecated] "Use other_func instead"
def higher_order(x): ...
# TODO: these diagnostics ideally shouldn't fire since we warn on the import
higher_order(DeprType) # error: [deprecated] "Use OtherType instead"
higher_order(depr_func) # error: [deprecated] "Use other_func instead"
# TODO: these diagnostics ideally shouldn't fire since we warn on the import
DeprType.__str__ # error: [deprecated] "Use OtherType instead"
depr_func.__str__ # error: [deprecated] "Use other_func instead"
If the items aren't imported and instead referenced using module.item then each use should produce
a warning.
module.py:
from typing_extensions import deprecated
@deprecated("Use OtherType instead")
class DeprType: ...
@deprecated("Use other_func instead")
def depr_func(): ...
main.py:
import module
module.DeprType() # error: [deprecated] "Use OtherType instead"
module.depr_func() # error: [deprecated] "Use other_func instead"
def higher_order(x): ...
higher_order(module.DeprType) # error: [deprecated] "Use OtherType instead"
higher_order(module.depr_func) # error: [deprecated] "Use other_func instead"
module.DeprType.__str__ # error: [deprecated] "Use OtherType instead"
module.depr_func.__str__ # error: [deprecated] "Use other_func instead"
If the items are instead star-imported, then the actual uses should warn.
module.py:
from typing_extensions import deprecated
@deprecated("Use OtherType instead")
class DeprType: ...
@deprecated("Use other_func instead")
def depr_func(): ...
main.py:
from module import *
DeprType() # error: [deprecated] "Use OtherType instead"
depr_func() # error: [deprecated] "Use other_func instead"
def higher_order(x): ...
higher_order(DeprType) # error: [deprecated] "Use OtherType instead"
higher_order(depr_func) # error: [deprecated] "Use other_func instead"
DeprType.__str__ # error: [deprecated] "Use OtherType instead"
depr_func.__str__ # error: [deprecated] "Use other_func instead"
Ideally a deprecated warning shouldn't transitively follow assignments, as you already had to "name" the deprecated symbol to assign it to something else. These kinds of diagnostics would therefore be redundant and annoying.
from typing_extensions import deprecated
@deprecated("Use OtherType instead")
class DeprType: ...
@deprecated("Use other_func instead")
def depr_func(): ...
alias_func = depr_func # error: [deprecated] "Use other_func instead"
AliasClass = DeprType # error: [deprecated] "Use OtherType instead"
# TODO: these diagnostics ideally shouldn't fire
alias_func() # error: [deprecated] "Use other_func instead"
AliasClass() # error: [deprecated] "Use OtherType instead"
If a dunder like __add__ is deprecated, then the equivalent syntactic sugar like + should fire a
diagnostic.
from typing_extensions import deprecated
class MyInt:
def __init__(self, val):
self.val = val
@deprecated("MyInt `+` support is broken")
def __add__(self, other):
return MyInt(self.val + other.val)
x = MyInt(1)
y = MyInt(2)
z = x + y # TODO error: [deprecated] "MyInt `+` support is broken"
If a dunder like __invert__ is deprecated, then the equivalent ~ operator should fire a
diagnostic.
from typing_extensions import deprecated
class MyBits:
@deprecated("MyBits `~` support is broken")
def __invert__(self):
return self
x = MyBits()
~x # error: [deprecated] "MyBits `~` support is broken"
If the operand's type is a union and the dunder is missing on some members, it's possibly unbound.
This should still report the deprecation on the members where it is found and is deprecated,
alongside unsupported-operator diagnostic.
from typing_extensions import deprecated
class MyBits:
@deprecated("MyBits `~` support is broken")
def __invert__(self):
return self
class NoBits: ...
def f(x: MyBits | NoBits):
# error: [unsupported-operator]
# error: [deprecated]
~x
A union reports a deprecated operator when any alternative is deprecated. An intersection reports deprecated operators only when every applicable implementation is deprecated.
from typing_extensions import deprecated
class Deprecated:
@deprecated("old inversion")
def __invert__(self) -> int:
return 1
class AlsoDeprecated:
@deprecated("another old inversion")
def __invert__(self) -> int:
return 2
class Ordinary:
def __invert__(self) -> int:
return 3
def mixed_union(value: Deprecated | Ordinary) -> None:
~value # error: [deprecated] "old inversion"
def mixed_intersection(value: Deprecated) -> None:
if isinstance(value, Ordinary):
~value
def deprecated_intersection(value: Deprecated) -> None:
if isinstance(value, AlsoDeprecated):
# error: [deprecated] "old inversion"
# error: [deprecated] "old inversion"
~value
A gradually typed comparison can produce an intersection of bool and Any. The unknown
alternative might provide a nondeprecated operator, so inverting it should not warn.
from typing import Any
def gradual_intersection(value: Any) -> None:
if value is None:
return
mask = value == 0
~mask
bool.__invert__ is one such case in typeshed. This applies both to bool literals and to
arbitrary values of type bool.
~True # error: [deprecated]
def f(x: bool):
~x # error: [deprecated]
Type variable constraints also should be checked.
from typing import TypeVar
from typing_extensions import deprecated
class First:
@deprecated("first")
def __invert__(self) -> int:
return 42
class Second:
@deprecated("second")
def __invert__(self) -> int:
return 42
T = TypeVar("T", First, Second)
def f(value: T) -> None:
# error: [deprecated] "first"
# error: [deprecated] "second"
~value
Deprecation reporting for one constraint does not depend on whether another constraint supports the operator or on the order of the constraints.
class Third: ...
U = TypeVar("U", Third, First)
V = TypeVar("V", First, Third)
def g(value: U) -> None:
# error: [unsupported-operator]
# error: [deprecated]
~value
def h(value: V) -> None:
# error: [unsupported-operator]
# error: [deprecated]
~value
A constraint that is itself a union may contain a deprecated operator even when that operator is missing from another union member.
W = TypeVar("W", First | Third, Second)
def nested_union(value: W) -> None:
# error: [unsupported-operator]
# error: [deprecated] "first"
# error: [deprecated] "second"
~value
A deprecated operator should also be reported when its signature cannot accept the implicit unary call.
class Invalid:
@deprecated("invalid inversion")
def __invert__(self, required: int) -> int:
return required
X = TypeVar("X", Invalid, Second)
def invalid_operator(value: X) -> None:
# error: [unsupported-operator]
# error: [deprecated] "invalid inversion"
# error: [deprecated] "second"
~value
Overloads can be deprecated, but only trigger warnings when invoked.
from typing_extensions import deprecated
from typing_extensions import overload
@overload
@deprecated("strings are no longer supported")
def f(x: str): ...
@overload
def f(x: int): ...
def f(x):
print(x)
f(1)
f("hello") # TODO: error: [deprecated] "strings are no longer supported"
If the actual impl is deprecated, the deprecation always fires.
from typing_extensions import deprecated
from typing_extensions import overload
@overload
def f(x: str): ...
@overload
def f(x: int): ...
@deprecated("unusable")
def f(x):
print(x)
f(1) # error: [deprecated] "unusable"
f("hello") # error: [deprecated] "unusable"