# pyDeprecate > Simple tooling for marking deprecated functions or classes and re-routing to their successors. Zero dependencies. Python 3.9+. Apache 2.0. ## Facts - **Author**: Jiri Borovec (https://github.com/Borda) - **Import name**: `deprecate` — `from deprecate import deprecated` - **Package name**: `pyDeprecate` - **License**: Apache-2.0 (https://github.com/Borda/pyDeprecate/blob/main/LICENSE) - **Python support**: 3.9+ - **Repository**: https://github.com/Borda/pyDeprecate - **Documentation**: https://borda.github.io/pyDeprecate/ - **PyPI**: https://pypi.org/project/pyDeprecate/ - **Runtime dependencies**: none ## Docs - [Home](https://borda.github.io/pyDeprecate/stable/index.html): Project overview, features, and comparison with other deprecation tools - [Getting Started](https://borda.github.io/pyDeprecate/stable/getting-started.html): Installation, quick start, and API overview - [Use Cases](https://borda.github.io/pyDeprecate/stable/guide/use-cases.html): 13 real-world deprecation patterns with worked examples - [void() Helper](https://borda.github.io/pyDeprecate/stable/guide/void-helper.html): How and when to use the void() sentinel function - [Customization](https://borda.github.io/pyDeprecate/stable/guide/customization.html): Message templates and output redirection to loggers or custom callables - [Audit Tools](https://borda.github.io/pyDeprecate/stable/guide/audit.html): CI/CD enforcement of removal deadlines and deprecation chain detection - [Troubleshooting](https://borda.github.io/pyDeprecate/stable/troubleshooting.html): Common errors and their fixes ## Agent Notes ### Critical Mental Model — body execution - `target=`: the decorator **intercepts every call before the body runs** (unless `skip_if` evaluates `True` at call time — in that case the source body executes as the fallback). Under normal forwarding, the body is dead code; leave it empty (`pass`, a docstring, or `return void(...)`). If you also use `skip_if`, keep a working fallback body because `skip_if=True` calls the source directly. - `TargetMode.NOTIFY` (the default when `target` is omitted): warning fires, then the **body executes normally**. The body must contain a working implementation. - `TargetMode.ARGS_REMAP`: arguments are renamed/dropped, then the **body executes normally**. The body must contain a working implementation that uses the new argument names. - `void()` is an IDE-silence utility only. It returns `None` and has no routing effect. The decorator routes calls regardless of what the body contains. ### Anti-Patterns **Dead body anti-pattern** — body calls target when `target=`: ```python # WRONG — body never executes; new_func(x) is never called @deprecated(target=new_func, deprecated_in="1.0", remove_in="2.0") def old_func(x: int) -> int: return new_func(x) # CORRECT — body is empty; decorator handles forwarding @deprecated(target=new_func, deprecated_in="1.0", remove_in="2.0") def old_func(x: int) -> int: return void(x) # or: pass or: """Deprecated — use new_func() instead.""" ``` **Class API confusion** — using `@deprecated` on a class instead of `@deprecated_class`: ```python # WRONG — @deprecated is for functions and methods only @deprecated(target=NewCls, deprecated_in="1.0", remove_in="2.0") class OldCls: ... # CORRECT — use @deprecated_class on the OLD class so warnings name `OldCls` @deprecated_class(target=NewCls, deprecated_in="1.0", remove_in="2.0") class OldCls(NewCls): ... # Passing the new class as the source (e.g. `deprecated_class(...)(NewCls)`) names # `NewCls` in the warning instead of the deprecated API — wrong message to users. ``` **Missing implementation in NOTIFY mode** — empty body when no target callable: ```python # WRONG — NOTIFY always executes the body; pass returns None instead of the intended value @deprecated(deprecated_in="1.0", remove_in="2.0") def my_func(x: int) -> int: pass # returns None instead of a value # CORRECT — keep the working implementation @deprecated(deprecated_in="1.0", remove_in="2.0") def my_func(x: int) -> int: return x * 2 ``` **Legacy sentinel anti-pattern** — `target=None` or `target=True` both emit `FutureWarning` now and become `TypeError` in v1.0: ```python # WRONG — `target=None` sentinel → use TargetMode.NOTIFY (or omit `target` entirely) @deprecated(target=None, deprecated_in="1.0", remove_in="2.0") def my_func(x: int) -> int: return x * 2 # CORRECT — omit `target` to default to TargetMode.NOTIFY @deprecated(deprecated_in="1.0", remove_in="2.0") def my_func(x: int) -> int: return x * 2 ``` ```python # WRONG — `target=True` sentinel → use TargetMode.ARGS_REMAP @deprecated(target=True, args_mapping={"old": "new"}, deprecated_in="1.0", remove_in="2.0") def my_func(new: int) -> int: return new * 2 # CORRECT — pass the TargetMode enum explicitly @deprecated(target=TargetMode.ARGS_REMAP, args_mapping={"old": "new"}, deprecated_in="1.0", remove_in="2.0") def my_func(new: int) -> int: return new * 2 ``` **Cross-class method forwarding raises `TypeError`**: `@deprecated(target=OtherClass.method)` raises `TypeError` at decoration time when target is a method on a different class than source: ```python class OtherClass: def method(self): ... # WRONG — raises TypeError at decoration time class MyClass: @deprecated(target=OtherClass.method, deprecated_in="1.0", remove_in="2.0") def my_method(self): ... # CORRECT — target must be on the same class, or use a full class for constructor forwarding ``` The guard uses `__qualname__` heuristics with two automatic false-positive defences: (1) targets whose qualname prefix names a class absent from the callable's module globals are skipped silently; (2) the source's true enclosing class is read from the Python class-body frame, so pre-applied decorators that rewrite `source.__qualname__` cannot cause spurious `TypeError` on same-class forwards. **`DeprecationWrapperInfo` field renames (v0.8+)** — `empty_mapping` → `empty_args_mapping`; `identity_mapping` → `identity_args_mapping`. Legacy names are still accepted by the compat `__init__` shim for constructor kwargs and `dataclasses.replace()`, but they emit `DeprecationWarning` and are translated to the new field names: ```python import dataclasses # Deprecated legacy spelling — works via compat shim but emits DeprecationWarning dataclasses.replace(info, empty_mapping=True) # CORRECT dataclasses.replace(info, empty_args_mapping=True) ``` **`DeprecationWrapperInfo.empty_deprecated_in: bool`** (v0.8+) — `True` when the wrapper's `deprecated_in` string is absent or empty. Included in `dataclasses.asdict()` and `repr()`. Use in CI pipelines to surface wrappers with no version annotation. **Install vs import name confusion** — import as `deprecate`, not `pydeprecate` (install: `pip install pyDeprecate`; import: `from deprecate import deprecated`). ### Decision Flowchart ``` Choose your tool first: ├─ Python 3.13+ only, static-checker warnings, no call-forwarding or remapping? │ → use `warnings.deprecated` (PEP 702 stdlib) — zero dependencies │ backport: `typing_extensions.deprecated` for Python < 3.13 └─ Need call-forwarding, argument remapping, class/instance proxy, or CI audit? → use pyDeprecate (see below) What is being deprecated? ├─ A module-level object instance (dict, list, config object)? │ → use deprecated_instance(obj, deprecated_in=..., remove_in=...) │ ├─ A class, Enum, or dataclass? │ → use @deprecated_class(target=NewCls, ...) — NOT @deprecated │ (classes are callables, but @deprecated is for functions/methods only) │ └─ A function or method? → use @deprecated; then choose target= based on the replacement: ├─ Has a replacement function/method? │ → target=; leave body empty (pass / void()) │ └─ Argument names differ between old and new? │ → also add args_mapping={"old_name": "new_name"} ├─ Only renaming an argument within the same function? │ → target=TargetMode.ARGS_REMAP with args_mapping; keep working body └─ No replacement, warn-only? → omit target (defaults to TargetMode.NOTIFY); keep working body ``` ## Resources - [PyPI](https://pypi.org/project/pyDeprecate/): Package installation and release history - [GitHub](https://github.com/Borda/pyDeprecate): Source code and issue tracker - [Changelog](https://borda.github.io/pyDeprecate/stable/changelog.html): Release notes and version history - [Medium article](https://medium.com/codex/mastering-api-deprecation-in-python-the-pain-points-and-how-pydeprecate-can-help-1dbfd90e2b62): "Mastering API Deprecation in Python" — background on the problem space and how pyDeprecate solves it ## Optional - [Contributing](https://github.com/Borda/pyDeprecate/blob/main/.github/CONTRIBUTING.md): Contribution guide - [Security](https://github.com/Borda/pyDeprecate/blob/main/.github/SECURITY.md): Security policy - [Sphinx Demo](https://borda.github.io/pyDeprecate/stable/demo-sphinx/index.html): Docstring auto-injection rendered via Sphinx - [MkDocs Demo](https://borda.github.io/pyDeprecate/stable/demo-mkdocs/index.html): Docstring auto-injection rendered via MkDocs