Compare Python Deprecation Tools🔗
Python has several ways to signal deprecation. The right choice depends on whether you need only a warning, static type checker visibility, or full runtime compatibility during an API migration.
Quick comparison🔗
| Tool | Best for | Runtime forwarding | Generator / async | Argument rename | Argument deprecation | Class/object alias | Testing helpers | CI audit | Static checker signal |
|---|---|---|---|---|---|---|---|---|---|
warnings.warn |
One-off internal warnings | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
warnings.deprecated |
Python 3.13+ static-checker visibility | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
deprecation |
Simple decorator warnings | ❌ | ❌ | ❌ | ❌ | ❌ | @fail_if_not_removed |
❌ | ❌ |
Deprecated |
Simple decorator warnings | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
| pyDeprecate (this library) | Full API migration compatibility | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✍️ |
✅ = supported
❌ = not supported
✍️ = possible but requires manual work — for pyDeprecate, stack @warnings.deprecated above @deprecated
Use warnings.warn when🔗
Use raw warnings for small internal messages where callers do not need a compatibility shim.
import warnings
def internal_hook() -> None:
warnings.warn("internal_hook is deprecated", DeprecationWarning, stacklevel=2)
Use warnings.deprecated when🔗
Use warnings.deprecated/typing_extensions.deprecated for static-checker-only visibility when runtime behavior does not need to change.
from typing_extensions import deprecated
@deprecated("Use detect_objects instead.")
def detect() -> None:
pass
Use pyDeprecate when🔗
Use pyDeprecate when old callers must keep working while receiving a deprecation warning.
from deprecate import deprecated
def detect_objects(value: int) -> int:
return value + 1
@deprecated(
target=detect_objects,
deprecated_in="1.2",
remove_in="2.0",
# FutureWarning: "The `detect` was deprecated since v1.2 in favor of `detect_objects`. It will be removed in v2.0."
)
def detect(value: int) -> int:
pass # body never runs — pyDeprecate intercepts all calls before reaching here
print(detect(1))
Migrating from warnings.warn🔗
Replace manual forwarding boilerplate with a single decorator.
Before: manual warning and forwarding
import warnings
def detect_objects(value: int) -> int:
return value + 1
def detect(value: int) -> int:
warnings.warn(
"detect is deprecated; use detect_objects instead.",
DeprecationWarning,
stacklevel=2,
)
return detect_objects(value)
print(detect(1))
After: pyDeprecate owns the migration contract
from deprecate import deprecated
def detect_objects(value: int) -> int:
return value + 1
@deprecated(
target=detect_objects,
deprecated_in="1.2",
remove_in="2.0",
# FutureWarning: "The `detect` was deprecated since v1.2 in favor of `detect_objects`. It will be removed in v2.0."
)
def detect(value: int) -> int:
pass # body never runs — pyDeprecate intercepts all calls before reaching here
print(detect(1))
Turning the warning into an error at a chosen version🔗
With plain warnings.warn the escalation is a manual raise you must remember to add. With pyDeprecate, as_exception=True raises DeprecatedError instead of warning, and a version comparison decides when it flips. No new API is involved: pass as_exception a boolean computed from your package version and a release of your choosing (ERROR_IN below).
from deprecate import DeprecatedError, TargetMode, deprecated_callable
PACKAGE_VERSION = (1, 3) # in a real package: parse your own `__version__`
ERROR_IN = (1, 3) # the release where callers must stop using the old name
@deprecated_callable(TargetMode.NOTIFY, "1.0", "2.0", as_exception=PACKAGE_VERSION >= ERROR_IN)
def detect_legacy(value: int) -> int:
return value
try:
detect_legacy(1)
except DeprecatedError as err:
print(err)
As the release-time backstop, pydeprecate expiry in CI exits 1 when a wrapper is past its remove_in deadline. See Fatal deprecations for the full behaviour.
Decision guide🔗
- Need only an internal warning: use
warnings.warn. - Need static checker visibility only: use
warnings.deprecatedon Python 3.13+. - Need public runtime compatibility: use pyDeprecate.
- Need to deprecate
async deffunctions, generator functions, or async generators: use pyDeprecate. - Need argument rename or dropped argument compatibility: use pyDeprecate with
TargetMode.ARGS_REMAP. - Need class, constant, or object alias compatibility: use
deprecated_classordeprecated_instance. - Need removal deadline checks in CI: use pyDeprecate audit tools.
- One-off internal warning with no forwarding or CI audit:
warnings.warnis sufficient.
Fair strengths in alternative tools worth considering🔗
To keep this comparison fair, here are capabilities where alternatives can be the better fit:
warnings.deprecated: native static diagnostics in mypy/pyright/IDEs without adding runtime wrappers; adds no per-call overhead when the warning category is suppressed.deprecation: includes the@fail_if_not_removedtest decorator for direct test-failure enforcement when removal deadlines are reached.Deprecated:deprecated.sphinxincludes@versionaddedand@versionchangeddecorators that inject Sphinx directives into docstrings for lifecycle annotation in Sphinx-built API docs.warnings.warn: no dependency and minimal surface area for quick internal notices where migration behavior is not needed.