NewYour coding agent can read the release notes before it upgrades.Set up the MCP server →
PyPI · #4763 most downloaded on PyPI
Python deprecation decorator: call forwarding, argument mapping, class proxying, CI audit. Zero deps.
Last release 13 days ago
21 Sep 2026
Release timing varies
gaps range from 8 days to 2 months
Nearly every release is documented
notes for 16 of 17 stable releases
1 version withdrawn
withdrawn after publishing
6 years old
24 releases · first in 2021
One column per quarter.
pyDeprecate v0.13.0 aligns __deprecated__ with PEP 702: it's now the plain conformant message string every PEP-702-aware tool expects, with pyDeprecat…
pyDeprecate v0.13.0 aligns __deprecated__ with PEP 702: it's now the plain conformant message string every PEP-702-aware tool expects, with pyDeprecate's own config moved to the new __deprecation_config__ attribute and read through the public get_deprecation_config(). Alongside the metadata split, a deprecation-governance lint (validate_deprecation_policy() / pydeprecate policy) checks that each deprecation gives callers a fair grace window before the removal PR lands, and every audit-scanning function gained an exclude= filter to keep frozen legacy trees out of package-wide scans.
__deprecated__ now follows PEP 702from deprecate import deprecated, get_deprecation_config
def new_func():
return "new behavior"
@deprecated(target=new_func, deprecated_in="1.0", remove_in="2.0")
def old_func():
return new_func()
get_deprecation_config(old_func).target is new_func # True — replaces old_func.__deprecated__.targetpydeprecate policy path/to/your/package
# exit 1 on a violation, e.g. too-short grace window or a message naming no replacementexclude= on every audit scanfrom deprecate import find_deprecation_wrappers
find_deprecation_wrappers("my_package", recursive=True, exclude=["my_package.tests", "*._legacy*"])__deprecated__ is now a plain PEP 702-conformant message string, not the DeprecationConfig object. The config object moved to __deprecation_config__. Code reading func.__deprecated__.target, .deprecated_in, .args_mapping, or any other config field, or checking isinstance(func.__deprecated__, DeprecationConfig), now gets a plain str and either raises AttributeError or evaluates False. Affects every wrapper kind — functions, methods, class proxies, instance proxies, and modules.
# BEFORE (v0.12 and earlier)
config = old_func.__deprecated__
target = config.target
# AFTER (v0.13+)
from deprecate import get_deprecation_config
config = get_deprecation_config(old_func)
target = config.targetget_deprecation_config() also reads the pre-v0.13 layout, so it works unchanged against objects built by an older pyDeprecate release.
validate_deprecation_policy() and the pydeprecate policy CLI subcommand check whether each deprecation was scheduled responsibly: a minimum grace window between deprecated_in/remove_in, and a warning message that names a replacement. Runs advisory-only inside pydeprecate all; the dedicated subcommand is the real gate. (#231)exclude on every audit scan — find_deprecation_wrappers(), validate_deprecation_expiry(), validate_deprecation_policy(), validate_deprecation_chains(), validate_mapping_compatibility(), and generate_deprecation_table() take a keyword-only exclude list of glob patterns over dotted module names; a matching module is never imported by the scan. (#231)get_deprecation_config(obj) metadata accessor — reads the v0.13 __deprecation_config__ and falls back to the pre-v0.13 __deprecated__ layout for mixed-version installs. (#230)pydeprecate coding-agent plugin for Codex and Claude Code — ships shared sunset and prune skills under plugins/pydeprecate/, packaged separately from the pip distribution. (#232)__deprecated__ now stores a PEP 702 message string, not the DeprecationConfig object. See Migration guide above. (#230)get_deprecation_config(obj) is not None instead of a bare __deprecated__ truthiness check — a foreign object decorated only with warnings.deprecated/typing_extensions.deprecated no longer gets mis-treated as a pyDeprecate wrapper. (#230)message_template placeholders that don't apply to the current target mode no longer raise KeyError — they render as empty strings instead. (#230)__deprecated__/__deprecation_config__ split, the deprecation policy lint, the exclude audit filter, and the shared Codex/Claude Code deprecation-workflow pluginFull changelog: v0.12.0...v0.13.0
validate_deprecation_policy() and pydeprecate policy. Checks whether each deprecation was scheduled responsibly against two independently disable-able rules: min_grace (remove_in must be one clean single-component bump beyond deprecated_in, at least a version-shaped delta away — "0.3" three minors by default, "1.0" one major, "0.0.2" two patches, or a one-key unit table {"major": 1}; a bare "1" is rejected as ambiguous; every spelling is converted up front into the strict deprecate.audit.GraceWindow(count, unit), which may also be passed directly; a mixed bump such as 1.2 → 2.3 never passes) and message_required (warning must name a replacement). Exports validate_deprecation_policy, PolicyRule, GraceWindow, and VersionBump. The pydeprecate policy CLI subcommand takes no --version, exits 1 on violations, 2 on a malformed --min-grace, and 0 with an install hint when the [audit] extra (packaging) is absent and message_required finds nothing to flag — message_required runs without packaging and can still exit 1 on its own. pydeprecate all runs the check in advisory-only mode — violations are printed as a [WARNING] but never change all's exit status. The CLI resolves each rule as flag → [tool.pydeprecate.policy] table in the nearest pyproject.toml (keys are the rule slugs; min-grace = { minor = 3 } or a quoted dotted delta; min-grace = false disables the window) → built-in default, prints a Policy: header naming each value's source, exits 2 on a malformed file value or an unreadable pyproject.toml (in policy and all alike), and warns on unknown keys. (#231)exclude for every audit scan. find_deprecation_wrappers(), validate_deprecation_expiry(), validate_deprecation_policy(), validate_deprecation_chains(), validate_mapping_compatibility() and generate_deprecation_table() take a keyword-only exclude list of fnmatch globs over full dotted module names; the scan itself never imports a matching package nor descends into it, and no wrapper reported under a matching module is returned. Every CLI subcommand takes --exclude (one pattern, comma-separated, or a list), defaulting to the exclude list of [tool.pydeprecate] in the nearest pyproject.toml, prints an Exclude: header naming the source, and exits 2 on a non-string value. The recursive walk replaces pkgutil.walk_packages (which imports a package before it can be skipped) with an iter_modules walk that checks exclusion first. (#231)get_deprecation_config(obj) metadata accessor. Reads the v0.13 __deprecation_config__ object and falls back to the pre-v0.13 __deprecated__ configuration layout for mixed-version applications. (#230)pydeprecate coding-agent plugin for Codex and Claude Code. Ships shared sunset and prune skills under plugins/pydeprecate/, with dual-host manifests and marketplace catalogs (.claude-plugin/, .codex-plugin/), separate from the pyDeprecate pip package. (#232)__deprecated__ now follows PEP 702 and stores a rendered message string. pyDeprecate's configuration moved to __deprecation_config__; audit and documentation integrations use get_deprecation_config() so legacy wrappers remain discoverable during migration. Griffe's RuntimeDocstrings extension now scopes its docstring-replacement guard to get_deprecation_config(obj) is not None instead of a bare __deprecated__ truthiness check, so a foreign object decorated only with warnings.deprecated/typing_extensions.deprecated is no longer mistaken for a pyDeprecate wrapper. (#230)message_template placeholders that don't apply to the current target mode no longer raise KeyError. Message rendering now probes the full public placeholder vocabulary up front and substitutes an empty string for any placeholder the target mode doesn't populate, instead of failing when a template references one. (#230)…and your IDE can see through them. One narrow breaking change, and it delivers on a TypeError that v0.11.0 already warned was coming.
pyDeprecate 0.12.0 finishes the family. You could already deprecate a function, a class, or an instance — but not a whole module, and a plain PEP 562 __getattr__ hook can't warn on attributes that already exist in __dict__. deprecated_module() closes that gap with three modes and warns on every public attribute access, star-imports included. Alongside it, @deprecated becomes an explicit crossroad: it defaults to the new TargetMode.AUTO, which figures out the mode at decoration time instead of quietly rendering your args_mapping inert, and the long-threatened TypeError for @deprecated on a class is formally withdrawn — that dispatch is permanent. deprecated_callable() is there when you want the strict opposite, and deprecated proxies finally leave breadcrumbs (__wrapped__, __signature__) so inspect, Sphinx, griffe, and your IDE can see through them. One narrow breaking change, and it delivers on a TypeError that v0.11.0 already warned was coming.
deprecated_module() — retire an entire moduleOne call at the bottom of the module you're retiring. It reassigns the module's __class__ to a wrapper that warns on every public attribute access — including attributes already in __dict__, which is exactly what a PEP 562 __getattr__ hook can't reach.
# old_calculator.py
from deprecate import deprecated_module
def add(a: float, b: float) -> float:
return a + b
deprecated_module(__name__, deprecated_in="2.0", remove_in="3.0", message_template="Use `new_calculator` instead.")
# old_calculator.add(1, 2) # warns: FutureWarning, returns 3Three modes: warn in place, redirect unknown lookups to a replacement module (with optional attrs_mapping), or expose the old sub-module name on the parent package. from old_calculator import * warns too — CPython's IMPORT_STAR routes each name through the same interception. Calling it twice with the same configuration is a no-op; calling it again with a different configuration warns and keeps the first. A pre-existing __getattr__ gets chained rather than clobbered.
Audit tooling comes along for the ride: find_deprecation_wrappers() discovers deprecated modules, and validate_deprecation_wrapper() takes module objects directly.
TargetMode.AUTO is the new @deprecated default@deprecated used to default to TargetMode.NOTIFY, which meant passing args_mapping without a target landed you in warn-only mode with an inert mapping and a misconfiguration flag. It now defaults to AUTO and resolves at decoration time from what you actually wrote:
from deprecate import deprecated
# A non-empty `args_mapping`, no explicit target → resolves to ARGS_REMAP
@deprecated(args_mapping={"coef": "coefficient"}, deprecated_in="1.0", remove_in="2.0")
def scale(value: float, coefficient: float = 1.0) -> float:
return value * coefficientAUTO never reaches DeprecationConfig — it resolves away before the config is frozen, so audit tooling always sees the concrete mode. It's the front-door default only; the strict forms reject it explicitly.
@deprecated on a class is permanent — and there's a strict form if you don't want thatSince v0.6 the warning said this would eventually become a TypeError. It won't. @deprecated on a class dispatches to deprecated_class with the full proxy and emits one informational UserWarning per module-qualified class name (silence it with stream=None). Only that notice goes away in v1.0.
If you want the opposite — a class handed to the decorator being an error — deprecated_callable() raises TypeError at decoration time, not call time. It shares every @deprecated parameter — with one default differing, since that is the whole point: deprecated_callable() defaults target to TargetMode.NOTIFY rather than TargetMode.AUTO, and rejects an explicit AUTO. It accepts functions, methods, lambdas, and the descriptor forms.
Deprecated proxies used to be opaque to introspection: inspect.signature saw the proxy, Sphinx documented the proxy, IDEs offered nothing. Every proxy now carries __wrapped__ and __signature__ pointing at the source, and reading either emits no warning.
from deprecate import DeprecationProxy, deprecated_class
class HttpClient:
def __init__(self, timeout: int = 30) -> None:
self.timeout = timeout
@deprecated_class(target=HttpClient, deprecated_in="0.2", remove_in="0.4")
class Client: ...
# `Client` is now a proxy object, not a class — annotate it against the public
# protocol instead of the private `_DeprecatedProxy`
LegacyClient: DeprecationProxy = Client
print(isinstance(LegacyClient, DeprecationProxy)) # True — @runtime_checkableThe breadcrumbs survive copy and deepcopy, and pickle for the proxies that were already picklable — a deprecated_class proxy that replaces its class's module-level name still can't be pickled, since pickle resolves the name to the proxy rather than the original class. Sources with no introspectable signature (a plain dict, C-level types) get __signature__ = None rather than an error, so wrapping never fails.
template_mgs is now message_template, and skip_if works on proxiesThe custom-notice parameter carried a typo (mgs for msg) since it was introduced. template_mgs still works as a deprecated alias that warns and forwards; it's removed in v1.0. And skip_if — previously callable-decorators-only — now works on deprecated_class() and deprecated_instance(), serving the wrapped source completely transparently while the condition holds.
One breaking change, and it only affects a configuration that never worked in the first place. Nothing was removed from the deprecated() signature — message_template was added to it.
@deprecated(target=TargetMode.ATTRS_REMAP) now raises TypeError for a class source. The mode needs attrs_mapping to redirect anything, and the front door has never exposed that keyword (it is deprecated_class-only). Previously a class source built a no-op proxy: the mode was recorded, nothing was ever remapped, and v0.11.0 emitted two UserWarnings about it — the class-dispatch notice, plus one naming attrs_mapping and stating outright that the proxy had "zero selective effect" and would become a TypeError in v1.0. This release delivers that. The callable path already raised.
from deprecate import TargetMode, deprecated, deprecated_class
# Before v0.12.0 — accepted, remapped nothing, and warned that it would become a TypeError
@deprecated(target=TargetMode.ATTRS_REMAP, deprecated_in="1.0", remove_in="2.0")
class Theme: ...
# v0.12.0 — TypeError at decoration time, pointing at the decorator that can do this.
# `attrs_mapping` values must name attributes that actually exist on the class.
@deprecated_class(attrs_mapping={"color": "colour"}, deprecated_in="1.0", remove_in="2.0")
class Theme:
colour = "red"If you had this in your codebase, check what mapping you intended before porting it — it was not being applied.
Deprecated: template_mgs → message_template on all four decorators. The old name warns and forwards through v0.12; passing both raises TypeError. Audit code reading DeprecationConfig.template_mgs keeps working through a read-only property alias.
Worth reviewing, not breaking: explicit target=TargetMode.NOTIFY together with args_mapping is still flagged at decoration time with a UserWarning, and the mapping stays inert — the same as in v0.11.0. It becomes a TypeError in v1.0, so it is worth fixing now: drop the explicit NOTIFY and let AUTO resolve the remap, name ARGS_REMAP outright, or drop the mapping if you really did mean warn-only.
Packaging: building from source now requires setuptools>=82 (up from >=70, across #225 and #227). Wheel installs are unaffected; the package still has zero runtime dependencies.
Full detail with before/after examples: releases/v0.12.0/MIGRATION.md.
deprecated_module() — PEP-562-inspired module-level deprecation in three modes, with audit-tool support. (#203)deprecated_callable() — strict callable-only form of @deprecated that rejects a class at decoration time. (#221)TargetMode.AUTO — decoration-time mode inference, now the @deprecated default. (#222)skip_if on deprecated_class() and deprecated_instance(). (#222)DeprecationProxy protocol for annotating proxies, replacing the private _DeprecatedProxy. (#226, #228)__wrapped__, __signature__) on every deprecated_class / deprecated_instance proxy. (#226)@deprecated on a class is now first-class and permanent, dispatching to deprecated_class with an informational notice instead of a threatened TypeError. (#222)message_template and skip_if passed through @deprecated on a class now reach the proxy — message_template was previously dropped silently on that path. (#222)@deprecated(target=TargetMode.ATTRS_REMAP) now raises TypeError for a class source instead of building a no-op proxy. (#222)target=True/target=False sentinels now resolve to an unset target and follow the same auto-resolve as an omitted target. (#222)TypeError naming deprecated_instance instead of crashing later on __name__. (#222)setuptools>=82; wheel installs unaffected, runtime dependencies still zero. (#227)template_mgs renamed to message_template on @deprecated, deprecated_callable, deprecated_class, and deprecated_instance. The old name warns and forwards; removed in v1.0. (#223)deprecated_module() module-level deprecation, the deprecated() class/callable crossroad with TargetMode.AUTO inference, and the strict deprecated_callable() form with the engine split into focused modules__wrapped__ / __signature__) on every deprecated proxy, making them legible to inspect, Sphinx autodoc, griffe, and IDEsFull changelog: v0.11.0...v0.12.0
deprecated_module() — PEP-562-inspired module-level deprecation. Call deprecated_module(__name__, deprecated_in=..., remove_in=...) once at the bottom of a module to install a __class__ reassignment to a wrapper type that emits FutureWarning on every public attribute access (including real attributes already in __dict__). Three modes: in-place warn (Mode 1), redirect to replacement module with optional attrs_mapping (Mode 2), and parent alias via deprecated_instance() (Mode 3). find_deprecation_wrappers() discovers deprecated modules via the __deprecated__ attribute; validate_deprecation_wrapper() accepts module objects directly. Double-call is idempotent (returns early); pre-existing __getattr__ is chained with a UserWarning. (#203)deprecated_callable() — strict callable-only form of @deprecated. Shares every @deprecated parameter and raises TypeError at decoration time when applied to a class. Accepts functions, methods, lambdas, and descriptors (classmethod / staticmethod / property); parallels the deprecated_class / deprecated_instance / deprecated_module family and is exported from the package. (#221)TargetMode.AUTO — decoration-time inference default for the @deprecated front door. @deprecated now defaults target=TargetMode.AUTO (was TargetMode.NOTIFY): an omitted target resolves from the rest of the configuration — a non-empty args_mapping resolves to TargetMode.ARGS_REMAP on functions and methods (an empty {} counts as no mapping) (previously this fell into the NOTIFY default and was flagged as a misconfiguration); a class source forwards the unset target so the proxy auto-resolve applies (args_mapping → ARGS_REMAP); no mapping resolves to warn-only (TargetMode.NOTIFY on callables, target=None recorded on class proxies). AUTO is never stored in DeprecationConfig; the strict forms deprecated_callable() and deprecated_class() reject target=TargetMode.AUTO with TypeError. (#222)skip_if on deprecated_class() and deprecated_instance(). The proxies gained the conditional-skip option previously available only on the callable decorators: while the bool (or zero-argument callable returning strict bool) evaluates True at access time, the proxy transparently serves the wrapped source — no warning, no attrs_mapping redirect, no args_mapping/args_extra handling, no target forwarding, and no read_only enforcement. A non-bool return raises the same TypeError as the callable form. (#222)DeprecationProxy protocol for annotating proxies. from deprecate import DeprecationProxy replaces annotating against the private _DeprecatedProxy. It is @runtime_checkable, so isinstance(obj, DeprecationProxy) works; being a data protocol, issubclass() raises TypeError. DeprecationProxy[T] is generic in the type produced by calling the proxy — deprecated_class and deprecated_instance return the concrete _DeprecatedProxy type in every call shape, which is what keeps the proxy's forwarded dunders (int(), with, await) visible to type checkers; annotate the assignment explicitly where the target type should flow into call sites. (#226, #228)deprecated_class and deprecated_instance proxies now carry __wrapped__ (the source object) and __signature__ (the source's signature), so inspect.unwrap, inspect.signature, Sphinx autodoc, griffe/mkdocstrings, and IDEs resolve through the proxy to the original. Reading either attribute emits no deprecation warning. Sources with no introspectable signature (a plain dict, C-level types) get __signature__ = None rather than an error — wrapping stays infallible. (#226)@deprecated on a class is now first-class and permanent. Applying @deprecated directly to a class dispatches to deprecated_class (full _DeprecatedProxy — isinstance/__class__ transparency, forwarding, budget semantics) and emits a one-time (per module-qualified class name, notice only — removed in 1.0) informational UserWarning — `@deprecated` on class `<Name>` now dispatches to `@deprecated_class`. — suppressed with stream=None. This replaces the v0.6 "will become a TypeError" warning; the threatened TypeError never ships. @deprecated_class stays the explicit, preferred form for classes. (#222)TargetMode.NOTIFY + a mapping stays flagged; auto-resolve applies only to an omitted target. Passing target=TargetMode.NOTIFY explicitly together with args_mapping (callables and proxies) or attrs_mapping (proxies) emits UserWarning at decoration time (TypeError in v1.0); the mode stays NOTIFY and the mapping is inert at runtime, preserved in audit metadata with misconfigured=True — explicit configuration is never silently rewritten. This flagging is unchanged from v0.11.0; what is new is that it no longer also applies to an omitted target, which now auto-resolves. Legacy proxy sentinels target=True (without a mapping) and target=False now resolve to an unset target and follow the same auto-resolve as an omitted target; warn-only proxies record DeprecationConfig.target=None. (#222)TypeError. Applying @deprecated to a plain object, a __call__ instance without __name__, or functools.partial of a class now raises TypeError naming deprecated_instance, instead of crashing later on __name__ access. (#222)deprecated() front door documented as the arguments common to both dispatch shapes (target, deprecated_in, remove_in, stream, num_warns, message_template, args_mapping, args_extra, skip_if, update_docstring, docstring_style). The class-only attrs_mapping is not among them and never has been — deprecated(attrs_mapping=...) raises TypeError (unexpected keyword argument) in v0.11.0 and v0.12.0 alike; use deprecated_class(attrs_mapping=...) directly. message_template and skip_if passed through @deprecated on a class are now forwarded to the proxy (message_template was previously dropped silently on the class-dispatch path). (#222)@deprecated(target=TargetMode.ATTRS_REMAP) now raises TypeError for a class source too. The mode needs attrs_mapping, which the front door does not expose, so it can never redirect anything through @deprecated; a class source previously built a no-op proxy — not silently: v0.11.0 emitted the class-dispatch notice plus a second UserWarning naming attrs_mapping and pre-announcing this TypeError. The callable path already raised as proxy-only. The error points to deprecated_class(attrs_mapping=...). (#222)setuptools>=82. The [build-system] requires floor moved from >=70 (via >=80 in #225); installing from an sdist on a build environment pinned below that will fail to bootstrap. Wheel installs are unaffected, and the package still declares zero runtime dependencies. (#227)template_mgs renamed to message_template. The custom-notice parameter on @deprecated, deprecated_callable, deprecated_class, and deprecated_instance was a typo (mgs for msg); it is now message_template. template_mgs stays as a deprecated keyword alias that emits a FutureWarning and forwards its value (passing both raises TypeError); it will be removed in v1.0. Audit code reading DeprecationConfig.template_mgs keeps working through a read-only property alias — the stored field is now message_template. (#223)…now catches expired class members by default. No breaking changes.
pyDeprecate 0.11.0 makes deprecated object proxies act like real drop-in replacements instead of leaky wrappers. Operators and protocol methods — arithmetic, iteration, context managers, async support — now forward to the wrapped object. Proxies survive copy/deepcopy/pickle (they used to blow up with RecursionError), and you can now subclass a deprecated_class alias directly instead of hitting a TypeError. Two identity fixes make isinstance behave the way you'd expect, and validate_deprecation_expiry() now catches expired class members by default. No breaking changes.
Arithmetic, comparisons, context managers, iteration, numeric conversion, os.fspath, format, and async protocols all delegate to the wrapped object now instead of raising TypeError.
from deprecate import deprecated_instance
DEFAULTS = deprecated_instance({"lr": 0.001}, deprecated_in="1.2", remove_in="2.0", read_only=True)
print(DEFAULTS["lr"]) # 0.001 — warns onceclass Child(OldName) on a deprecated_class alias used to raise a confusing metaclass error. It now resolves cleanly to the active replacement class.
validate_deprecation_expiry() now scans class members by defaultThe include_members default flipped from False to True, matching find_deprecation_wrappers(). If your CI expiry check was missing expired methods, constructors, or properties, it'll catch them now — pass include_members=False if you want the old, narrower scope back.
__class__, isinstance/issubclassA deprecated proxy's __class__ now reports the wrapped object's real type, so isinstance checks against it work as expected; type(proxy) still tells you it's a proxy. Passing an instance proxy as the second argument to isinstance/issubclass now raises TypeError instead of silently returning False.
copy, deepcopy, and pickleThese used to crash with RecursionError. deprecated_instance proxies over plain objects are now fully picklable; deprecated_class alias proxies may still raise PicklingError — use copy.deepcopy for those instead.
No breaking changes in this release. Nothing was removed or renamed.
Three behavior changes are worth a quick look. None require action for typical use — the last two only matter if your code depends on the specific old behavior:
validate_deprecation_expiry() now scans class members by default — pass include_members=False to restore the old scope.__class__ now reports the wrapped type. If you're detecting a proxy by checking obj.__class__ is _DeprecatedProxy, switch to type(obj) is _DeprecatedProxy instead.isinstance/issubclass with an instance proxy as the second argument now raises TypeError instead of silently returning False.os.fspath, format, and async protocols now delegate instead of raising TypeError; binary operators still return NotImplemented where Python expects it, and in-place operators (+=) rebind to the plain result. (#214)validate_deprecation_expiry() default include_members flipped from False to True — your CI expiry gate now scans class members by default; pass include_members=False to restore the old scope. (#210)__class__ now reports the wrapped object's type — this gives you real isinstance transparency for JSON encoders, validators, functools.singledispatch, and similar; type(proxy) still tells you it's a proxy. (#210)repr, str, ==, hash) now route through the active object — previously they used the deprecated source while attribute/item/call access used the active target, which was inconsistent. (#216)isinstance/issubclass with an instance proxy as the second argument now raise TypeError — previously this returned False silently, which could hide bugs. Class-alias proxies are unaffected. (#216)remove_in instead of skipping it silently. (#216)hasattr, copy/deepcopy, reading dunder attributes — no longer counts against your warning limit. (#210)TypeError on every call in notify-only and self-remapping modes (both sync and async). (#210)*args are now forwarded to callable targets instead of silently dropped. (#210)deprecated_class(args_mapping=...). (#210)pyproject.toml. (#210)find_deprecation_wrappers(recursive=True)) now survive submodules that raise errors other than ImportError on import. (#210)copy.copy, copy.deepcopy, and pickle. (#212)num_warns) is now tracked safely when multiple threads call a deprecated function for the first time at once. (#214)TypeError. (#214)all/status no longer fail on plain directories that lack an __init__.py. (#215)@deprecated (missing parens) now raises a clear TypeError when the first argument isn't callable. (#216)template_mgs with a bare %-conversion is now rejected with ValueError at decoration time instead of failing later. (#216)args_mapping, args_extra, and attrs_mapping are now copied defensively when you apply the decorator, so later mutation of the originals can't leak through. (#216)@property/@classmethod/@staticmethod-decorated methods. (#216)stream that raises TypeError internally is no longer invoked twice. (#216)**kwargs/*args target gives you the curated error message again. (#216)_-prefixed) and dunder members instead of skipping them. (#216)AttributeError. (#216)1.2.3+cuda) now parse correctly instead of losing that segment. (#216)Full changelog: v0.10.1...v0.11.0
proxy + 1), comparison/ordering, context managers (with proxy:), iteration (next, reversed), numeric conversion (int/float/round/abs), os.fspath, format, and the async protocols (async with, async for, await) now delegate to the active object instead of raising TypeError. Binary operators preserve NotImplemented semantics, so unsupported-operand errors surface normally. The warn-policy contract is documented in the _DeprecatedProxy docstring: data use warns (within the num_warns budget), cheap probes stay silent. Caveat: in-place operators (+=, -=, *=, …) return the active object's result rather than a re-wrapped proxy — after x += 1, the name x is rebound to a plain int and all subsequent uses are silent even if the deprecation window has not closed. (#214)class Child(OldName) on a deprecated_class alias previously raised a confusing metaclass arity TypeError; __mro_entries__ now resolves the alias to the active class and emits the deprecation warning (subclassing is a use of the deprecated name), respecting the warn budget and staying silent for attrs_mapping-only and args_mapping-only proxies. (#214)Performance
DeprecationConfig instead of re-derived on every call (uncached inspect.getfullargspec removed from the hot path); forwarded-call overhead drops from ~10.4 µs to ~4.3 µs with no behavior change. (#214)Proxy identity
proxy.__class__ now reports the wrapped object's type for isinstance transparency. _DeprecatedProxy exposes a __class__ property returning the active object's type, so type checks in downstream code (JSON encoders, validators, functools.singledispatch) keep working when an object is wrapped; type(proxy) still reveals the proxy. Code that previously detected the proxy via obj.__class__ is _DeprecatedProxy should use type(obj) is _DeprecatedProxy instead. (#210)repr(), str(), ==, and hash() on a target-forwarding proxy previously used the deprecated source while attribute/item/call access used the active target, so a proxy could compare equal to an object it never served; all four now route through the active object for consistency. (#216)isinstance / issubclass with an instance proxy as the second argument now raise TypeError. Using a deprecated_instance proxy (one wrapping a value rather than a class) as the second argument to isinstance/issubclass previously returned False silently, hiding the misuse; it now raises the same TypeError the builtins raise. Class-alias proxies are unaffected. (#216)Audit & expiry
validate_deprecation_expiry() now scans class members by default (include_members=True). Previously the expiry gate defaulted to include_members=False while find_deprecation_wrappers() defaulted to True, so CI gates silently skipped expired deprecated methods, constructors, classmethods, staticmethods, and properties. The flip can only surface additional expired wrappers — pass include_members=False explicitly to restore the old scope. (#210)remove_in instead of skipping it silently. validate_deprecation_expiry() (and the CLI expiry gate) previously dropped wrappers whose remove_in version could not be parsed, leaving them permanently un-expirable with no signal; such wrappers now emit a UserWarning naming the callable while the rest of the scan continues. (#216)Audit
find_deprecation_wrappers previously reported a wrapper once per importing module (e.g. once under the package root re-export and once under its defining submodule), inflating expiry counts and table rows; wrappers are now attributed to their defining module and deduplicated by identity across the scan. (#215)target is itself a deprecated proxy previously emitted a spurious FutureWarning from inside the audit tooling, consumed the proxy's warn budget, and printed a fabricated module path; proxy targets are now read via static metadata access. (#215)find_deprecation_wrappers(recursive=True) previously aborted the whole scan when any submodule raised a non-ImportError at import time (e.g. RuntimeError from an optional dependency); such submodules are now skipped with a warnings.warn naming the module and error. (#210)_scan_class skipped every _-prefixed member except __init__, so a deprecated private method or dunder could never be flagged as expired; private/dunder members that carry deprecation metadata are now included. (#216)deprecated_instance/deprecated_class proxy whose target is its own wrapped object was reported as effective because the self-reference check compared against the proxy rather than the wrapped object; it is now flagged as a no-op self-reference. (#216)getattr(..., default), which only suppresses AttributeError; a third-party object whose __getattr__ raised something else (e.g. a lazy proxy raising RuntimeError) aborted the whole scan. Such failures are now treated as "no metadata". (#216)CLI
all and status no longer fail on plain directories. Scanning a directory without __init__.py exited 1 from the status-table step even when every check passed; module-name resolution is now lazy and a status-rendering failure cannot change the aggregate exit code. (#215)importlib.metadata lookup previously failed for packages like pyDeprecate itself (import deprecate, distribution pyDeprecate); the import name is now mapped via packages_distributions(), and expiry prints an explicit note when it runs without a resolved version. (#215)pyproject.toml. When the scan path is an importable module name rather than a filesystem path, expiry/status previously walked up from the current directory and could gate against whatever project the shell happened to be in; filesystem-based detection now runs only for real paths. (#210)sys.exit inside the Fire invocation previously preempted Fire's unconsumed-argument check, so a typo such as --verison was dropped and the command ran with defaults; the CLI now lets Fire report the unconsumed flag and exits non-zero. (#210)encoding is None no longer crashes UTF-8 setup. _ensure_utf8_streams called .lower() on a possibly-None encoding attribute, raising AttributeError for some redirected streams; a None encoding is now tolerated. (#216)sys.exit(str(exc)), which printed nothing (exit 1) for a message-less exception; the exception type now prefixes the exit message so CI shows what failed. (#216)Call forwarding & signatures
TypeError on every call. @deprecated on def f(a, /, b=2) previously failed at call time in the default notify mode and TargetMode.ARGS_REMAP because positional-only arguments were re-passed as keywords; they are now split back out positionally for both sync and async sources. (#210)*args are now forwarded to callable targets instead of being silently dropped. A *args-declaring source forwarding to a target previously discarded everything past the named positionals (old_sum(1, 2, 3) returned 1); the positional tail is now forwarded, and incompatible targets raise the curated mapping TypeError. (#210)TypeError. The same signature-order dispatch replaces the setattr fallback in deprecated_class(args_mapping=...), which also failed for required positional-only parameters and frozen dataclasses. (#210)TypeError. The cross-class guard exists to prevent self carrying the wrong type, but staticmethods have no self; @deprecated(target=NewCls.compute) on a staticmethod forwarding to another class's staticmethod is now allowed. (#214)@property/@classmethod/@staticmethod wrapping pushed out of reach — silently disabling the check; a bounded frame walk now locates the class body regardless of descriptor frames. (#216)**kwargs/*args target yields the curated message again. The internal signature helper leaked *args/**kwargs names into caller-argument validation, producing a raw TypeError instead of the "argument not accepted by target" message; the variadic names are now excluded as documented. (#216)Proxy, decoration & config
num_warns quota is now thread-safe. Concurrent first calls to a shared wrapper could each pass the quota check before any counter increment, emitting up to one warning per thread instead of the configured budget; the warn path now synchronizes on a per-wrapper lock, so exactly num_warns warnings are emitted under concurrency. (#214)attrs_mapping-only deprecated class no longer emits a class-level warning. TargetMode.ATTRS_REMAP scopes the deprecation to the listed attributes, yet plain instantiation fired the blanket FutureWarning and consumed the warn budget; construction is now silent and only deprecated-attribute access warns. (#214)copy.copy, copy.deepcopy, and pickle. Copying or pickling any deprecated_class/deprecated_instance proxy previously crashed with RecursionError — the _cfg property and __getattr__ fell into infinite mutual recursion on half-initialized instances. Proxies now implement the copy/pickle protocol and reconstruct a functional proxy. Note: deprecated_instance proxies wrapping plain objects (dicts, lists) are fully picklable; deprecated_class proxies may raise PicklingError when the decorated class name is replaced by the proxy (the common alias pattern), because pickle cannot find the original class by reference. (#212)hasattr() probes on missing attributes, copy.deepcopy protocol lookups, and dunder access (e.g. __mro__ reads by doc tools) previously emitted the deprecation warning and exhausted the default num_warns=1 budget before any real usage. Warnings now fire only on successful non-dunder attribute access. (#210)@deprecated (no parentheses) now raises a clear TypeError when the first argument is not callable. Forgetting the call parentheses previously surfaced as a cryptic AttributeError: 'int' object has no attribute '__name__' on the first call; the decorator now explains that it must be called with arguments. (#216)template_mgs with a bare %-conversion is now rejected at decoration time. A template containing %s/%d (rather than a %(name)s mapping key) silently rendered the whole substitution dict into the warning; such templates now raise ValueError when the decorator is applied. (#216)args_mapping, args_extra, and attrs_mapping are defensively copied at decoration time. The frozen configuration previously aliased the caller's dict, so mutating it after decoration could silently change forwarding behavior (or introduce a redirect cycle that validation had already rejected); the mappings are now copied. (#216)TypeError internally is no longer invoked twice. Both the decorator and proxy warning paths called the stream with stacklevel and caught TypeError to retry without it — which also swallowed a TypeError raised inside a stacklevel-accepting stream and re-ran it (duplicate log/print). The paths now decide once, via a cached signature probe, whether the stream accepts stacklevel. (#216)_normalize_version_string ran its pre/post/dev label rule over the whole string, mangling a legitimate local version such as 1.2.3+cuda into 1.2.3+cuda0; the local segment (after +) is now split off and re-attached verbatim, and only a single leading v/V is stripped. (#216)pyDeprecate 0.10.1 closes two correctness gaps and ships one new audit diagnostic. Callable deprecation cycles now raise a clear RuntimeError instead…
pyDeprecate 0.10.1 closes two correctness gaps and ships one new audit diagnostic. Callable deprecation cycles now raise a clear RuntimeError instead of crashing with a RecursionError. The inner_order_property flag in audit results lets CI pipelines detect the silent setter/deleter gap from inner-order @property @deprecated stacking, and the new opt-in from deprecate import property catches that same mistake at class-body evaluation time — before any instance is created.
No migration required. All changes are additive.
RuntimeErrorA target= chain that loops back (A → B → A) previously caused unbounded recursion. The decorator now detects re-entry via a ContextVar guard and raises a named error immediately. Async tasks each get an independent detection set — no false positives from concurrent coroutines.
@deprecated(deprecated_in="1.0", remove_in="2.0", target=lambda: cycle_b())
def cycle_a(): ...
@deprecated(deprecated_in="1.0", remove_in="2.0", target=cycle_a)
def cycle_b(): ...
cycle_a()
# RuntimeError: Circular deprecation cycle detected: `cycle_a` re-entered via its own target chain.inner_order_property audit flagfind_deprecation_wrappers() now sets inner_order_property=True when a plain property has a @deprecated-wrapped fget — the inner-order shape where only the getter warns; setters and deleters added via @value.setter remain silently unprotected.
results = find_deprecation_wrappers(my_module)
flagged = [r for r in results if r.inner_order_property]
# Add to CI: assert not flagged, "Inner-order @property @deprecated found"property replacementfrom deprecate import property shadows the builtin with _StrictProperty, which raises TypeError at class-body time when handed an already-@deprecated getter — turning the silent footgun into a loud, immediate failure.
from deprecate import deprecated
from deprecate import property # opt-in strict mode for this module
class MyModel:
@property # TypeError raised here —
@deprecated(deprecated_in="1.0", remove_in="2.0") # before any instance is created
def old_value(self): ...Star imports (from deprecate import *) are unaffected — strictness is per-module opt-in only.
DeprecationWrapperInfo.inner_order_property flag — find_deprecation_wrappers() sets True for inner-order @property @deprecated where only fget warns; CI pipelines can filter on this field. (#201)property via from deprecate import property — raises TypeError at class-definition time when a getter already carries @deprecated metadata; star imports unaffected. (#201)RuntimeError at call time — ContextVar re-entrancy guard replaces unbounded recursion; async cycle detection avoids false positives from concurrent tasks. (#200)_StrictProperty TypeError message references correct module path — error now points to deprecate.deprecation._StrictProperty. (#201)Full changelog: v0.10.0...v0.10.1
DeprecationWrapperInfo.inner_order_property flag. find_deprecation_wrappers() now sets inner_order_property=True when a plain property (not _DeprecatedProperty) has a @deprecated-wrapped fget — the inner-order @property @deprecated shape where only the getter warns; setters and deleters added via @value.setter / @value.deleter remain silently unprotected. CI pipelines can filter on this field to reject the pattern. (#201)property replacement. from deprecate import property now exports _StrictProperty, a property subclass that raises TypeError at class-definition time when handed an already-@deprecated getter (inner-order detection). Import it in modules that want compile-time enforcement; star imports (from deprecate import *) are unaffected. (#201)RuntimeError at call time. Callable target chains that form a cycle (A → B → A) previously caused unbounded recursion and a RecursionError. The decorator now detects the cycle via a ContextVar re-entrancy guard and raises a clear RuntimeError naming the circular path. Async deprecation cycle detection was also improved to avoid false positives from concurrent tasks sharing a threading-local guard. (#200)_StrictProperty TypeError message now references the correct module path. The error previously pointed to deprecate._StrictProperty; corrected to deprecate.deprecation._StrictProperty, which is the actual import path. (#201)No breaking changes. All existing code continues to work.
v0.10.0 brings fine-grained attribute-level deprecation to deprecated_class via a new attrs_mapping parameter — deprecate individual attribute names (reads, writes, deletes) with per-attribute warning budgets and, for dataclasses, automatic constructor-kwarg expansion. The release also closes two coverage gaps: @deprecated @property now wraps the setter and deleter in addition to the getter, and raw staticmethod/classmethod descriptors are accepted as target without the .__func__ workaround. A correctness fix ensures calls to targets with positional-only parameters no longer raise TypeError.
deprecated_class(attrs_mapping={...}) — selective attribute deprecationDeprecate individual attribute names with per-attribute budgets. Reads, writes, and deletes each fire FutureWarning independently. None as the redirect value means warn-only (no rename). TargetMode.ATTRS_REMAP is the corresponding mode. (#191)
from deprecate import deprecated_class
@deprecated_class(attrs_mapping={"color": "colour"}, deprecated_in="2.0", remove_in="3.0")
class Palette:
colour: str = "red" # canonical name
size: int = 10 # unlisted — always silent
print(Palette.color) # warns: FutureWarning → redirected to colour
print(Palette.colour) # silentattrs_mapping auto-expandWhen the wrapped class is a @dataclass, one attrs_mapping entry automatically covers both attribute access and constructor kwargs — no separate args_mapping needed.
from dataclasses import dataclass
from deprecate import deprecated_class
@deprecated_class(attrs_mapping={"px": "x", "py": "y"}, deprecated_in="2.0", remove_in="3.0")
@dataclass
class OldPoint:
x: float = 0.0
y: float = 0.0
pt = OldPoint(px=1.0, py=2.0) # warns on px and py → x=1.0, y=2.0Explicit args_mapping entries always win over auto-expanded ones. (#193)
@deprecated @property wraps fset and fdelOuter-order @deprecated @property now wraps all three accessors. Previously only fget fired a warning. Chain-style @value.setter / @value.deleter re-wraps automatically via the new _DeprecatedProperty subclass.
from deprecate import deprecated
class Config:
def __init__(self) -> None:
self._timeout: int = 30
@deprecated(deprecated_in="1.0", remove_in="2.0")
@property
def timeout(self) -> int:
return self._timeout
@timeout.setter
def timeout(self, value: int) -> None:
self._timeout = value
@timeout.deleter
def timeout(self) -> None:
del self._timeout
cfg = Config()
_ = cfg.timeout # warns: FutureWarning (read)
cfg.timeout = 60 # warns: FutureWarning (write)
del cfg.timeout # warns: FutureWarning (delete)Inner order (@property @deprecated) still wraps fget only. (#190)
staticmethod/classmethod descriptors as targetInside a class body, pass the new method directly as target=new_method — no .__func__ needed. (#192)
from deprecate import deprecated, void
class Compute:
@staticmethod
def area(radius: float) -> float:
return 3.14159 * radius**2
@staticmethod
@deprecated(target=area, deprecated_in="1.0", remove_in="2.0")
def surface(radius: float) -> float:
"""Deprecated — use area() instead."""
return void(radius)
print(Compute.surface(3.0)) # warns: FutureWarning, returns area(3.0)@deprecated forwards calls to POSITIONAL_ONLY targetsPreviously raised TypeError at call time when target declared positional-only parameters (def fn(x, /)). Now detects at decoration time, emits UserWarning, and splits call dispatch positionally. (#194)
from deprecate import deprecated
def new_fn(value: float, /) -> float:
return value * 2
@deprecated(target=new_fn, deprecated_in="1.0", remove_in="2.0")
def old_fn(value: float) -> float: ...
result = old_fn(value=5.0) # warns: FutureWarning; calls new_fn(5.0) correctly
print(result) # 10.0No breaking changes. All existing code continues to work.
If you use @deprecated @property with a setter or deleter and run filterwarnings=error::FutureWarning: setter and deleter writes now correctly fire FutureWarning — they were silently skipped before (bug). Tests that write to or delete a deprecated property will now raise as expected. To intentionally keep only the getter warned, switch to inner order (@property @deprecated).
deprecated_class(attrs_mapping={...}) for selective attribute deprecation. Deprecated attribute names emit FutureWarning on read, write, and delete with per-attribute warning budgets. None as the redirect value means warn-only. TargetMode.ATTRS_REMAP is the corresponding mode. Multi-hop chains allowed; cycles raise ValueError at decoration time. (#191)deprecated_class stacking now supported. Two @deprecated_class decorators on the same class work correctly — isinstance() delegates through the chain, instantiation emits at most one warning. (#193)attrs_mapping auto-expand. Single attrs_mapping entry covers both attribute access and constructor kwargs on a @dataclass. Explicit args_mapping always wins. (#193)validate_mapping_compatibility() audit function. Returns all deprecated_class proxies whose args_mapping remaps to POSITIONAL_ONLY constructor parameters. Use in CI alongside validate_deprecation_expiry. (#193)@deprecated @property wraps fset and fdel. Outer-order decoration now covers all three accessors via the new _DeprecatedProperty subclass. (#190)staticmethod/classmethod descriptors accepted as target. _normalize_target unwraps automatically inside class bodies. (#192)@deprecated correctly forwards calls to targets with POSITIONAL_ONLY parameters. Detects at decoration time, emits UserWarning, splits call dispatch positionally. args_mapping applied before split. (#194)args_mapping precedence: explicit new-name always wins. When caller passes both deprecated old name and new name simultaneously, new-name value wins regardless of call-site order. (#198)Full changelog: v0.9.0...v0.10.0
Class attribute & dataclass mapping
deprecated_class(attrs_mapping={...}) for selective attribute deprecation. Deprecated attribute names emit FutureWarning on read, write, and delete with per-attribute warning budgets. None as the redirect value means warn-only (no rename). TargetMode.ATTRS_REMAP is the corresponding mode — can be combined with a callable target to redirect attribute access across class boundaries. Multi-hop chains and fan-in renames allowed; cycles raise ValueError at decoration time. (#191)attrs_mapping auto-expand. When the wrapped class is a @dataclass, a single deprecated_class(attrs_mapping={"old_field": "new_field"}) call automatically generates the corresponding args_mapping entry so both attribute access (obj.old_field) and constructor kwargs (DC(old_field=5)) emit FutureWarning from one decorator. Explicitly-provided args_mapping keys always win over auto-expanded entries. For non-dataclass targets, attrs_mapping covers attribute access only. (#193)Descriptors & property accessors
target= now accepts raw staticmethod / classmethod descriptors directly. Inside a class body the new method is still a raw descriptor (not yet bound); passing it as target=new_method no longer requires the explicit .__func__ suffix. _normalize_target unwraps the descriptor automatically. For classmethod descriptors the symmetric same-class pattern is supported (both deprecated and replacement are classmethods); asymmetric usage raises TypeError at decoration time. (#192)@deprecated @property now wraps fset and fdel with FutureWarning. Applying @deprecated on the outside of @property (outer order, or explicit deprecated(...)(property(fget, fset, fdel))) now wraps all three accessors. Previously, only fget emitted a warning; fset and fdel were silently passed through. Consumers running filterwarnings=error::FutureWarning that wrote to or deleted a deprecated property will now see FutureWarning errors — use inner-order (@property @deprecated) or decorate only fget directly if you want a silent setter/deleter. Chain-style rebinding via @value.setter / @value.deleter is fully supported through the new _DeprecatedProperty subclass. (#190)Stacking & audit
deprecated_class stacking is now supported. Two @deprecated_class decorators applied to the same class (each with its own attrs_mapping and version pair) now work correctly: isinstance() resolves through the proxy chain, instantiation emits at most one warning instead of two, and the type annotation accepts _DeprecatedProxy without a cast. Stacking ATTRS_REMAP outer + ARGS_REMAP inner is also supported: the inner proxy no longer emits a spurious global warning on attribute access — TargetMode.ARGS_REMAP now correctly restricts its warnings to call-time argument remapping only. No-target two-layer stacking (both layers deprecating the class in-place without forwarding to a different type) is also supported: the outer ATTRS_REMAP proxy delegates __call__ to the inner ARGS_REMAP proxy without firing a second global warning. (#193)validate_mapping_compatibility() audit function. Returns list[DeprecationWrapperInfo] for all deprecated_class proxies whose args_mapping remaps deprecated names to POSITIONAL_ONLY constructor parameters — those proxies fall back to setattr at call time instead of forwarding via kwargs. Use in CI to detect configurations that silently degrade to attribute assignment. (#193)@deprecated now correctly forwards calls to targets with POSITIONAL_ONLY parameters. When a callable target declares any parameter as positional-only (def new_fn(x, /): ...), the decorator previously raised TypeError at call time because all arguments were forwarded as kwargs. The decorator now detects POSITIONAL_ONLY params at decoration time, emits a UserWarning naming the affected parameters, and splits the call-time dispatch so those values are forwarded positionally. args_mapping remaps applied before the split — remapped names that land on a POSITIONAL_ONLY target param are handled correctly. The thin-adapter pattern (def new_fn_compat(x): return new_fn(x) as target) remains valid and suppresses the UserWarning. (#194)args_mapping precedence: explicit new-name always wins when both old and new kwargs passed. When a caller passed both the deprecated old argument name and the new name simultaneously (e.g. fn(val=5, new_val=6)), the remapped old-name value previously overwrote the explicit new-name value due to dict-comprehension last-write-wins ordering. The explicit new-name value now always wins, regardless of argument order at the call site. Affects @deprecated with target=TargetMode.ARGS_REMAP or a callable target, and deprecated_class() with args_mapping. (#198)One breaking change: the CLI flag --skip_errors was renamed to --exit-zero across all four subcommands ( check , expiry , chains , all ). Update any C…
pyDeprecate 0.9.0 adds async support, fixes descriptor ordering, and introduces Markdown audit tables — @deprecated now wraps async def coroutines, async generators, and sync generators correctly, decorator order on @classmethod and friends no longer matters, and pydeprecate status renders Markdown audit reports you can commit to your docs.
v0.9.0 closes three long-standing gaps in @deprecated. Sync generators, async coroutines, and async generators are all wrapped correctly — sync generators fire the warning eagerly at call time instead of after the first next(), inspect.iscoroutinefunction(wrapper) returns True for async coroutines, and async generators no longer raise a UserWarning at decoration time.
Descriptor decorator order is no longer a footgun. Both @classmethod @deprecated and @deprecated @classmethod (and the equivalent for @staticmethod) produce a working classmethod(deprecated_wrapper) — the descriptor is unwrapped, the inner function is deprecated, and the result is re-wrapped. find_deprecation_wrappers() looks inside descriptor members too, so audit scans no longer miss wrapped classmethods or staticmethods.
A new generate_deprecation_table() function and pydeprecate status CLI subcommand render Markdown reports grouped by module and API type (function, method, classmethod, staticmethod, property, class, instance) — with compact and matrix styles, lifecycle classification via the new DeprecationStatus enum, and --output FILE for committable artifacts.
One breaking CLI change: --skip_errors was renamed to --exit-zero across all four subcommands. The flag was introduced in v0.8.0 and renamed because the old name reads as if it swallows exceptions; the new name matches ruff, pylint, and shellcheck convention and describes the behavior plainly. Update CI scripts before upgrading. v0.8.0's target=None / target=True / DeprecationWrapperInfo field deprecations are still active and are scheduled for removal in v1.0.
@deprecatedDecorating a generator function (def fn(): yield ...) now emits the deprecation warning eagerly at call time — before the first next() — consistent with regular function behavior. The generator body executes lazily as normal when iterated. All three TargetMode variants work transparently; no isgeneratorfunction check is needed at the call site.
from deprecate import deprecated
@deprecated(deprecated_in="0.9", remove_in="1.0")
def stream_legacy_rows():
for row in _legacy_source():
yield row
it = stream_legacy_rows() # FutureWarning fires eagerly here
for row in it: # iteration runs lazily, unchanged
process(row)async def coroutine and async-generator support@deprecated now wraps async def coroutines and async def + yield async generators with no changes needed at the call site for direct use. The async-coroutine wrapper is itself async def, so inspect.iscoroutinefunction(wrapper) returns True and the warning fires when the coroutine is awaited. For async generators, the wrapper is a sync callable that fires the warning eagerly at call time and returns the async-generator object — iterate with async for. Note: inspect.isasyncgenfunction(wrapper) returns False for the async-generator case (the wrapper is sync), so frameworks that branch on that flag to decide how to call the function may need a thin async def passthrough.
import inspect
from deprecate import deprecated
@deprecated(deprecated_in="0.9", remove_in="1.0")
async def fetch_legacy(url: str) -> bytes:
return await _http_get(url)
assert inspect.iscoroutinefunction(fetch_legacy) # True
# FutureWarning fires here, at await time:
data = await fetch_legacy("https://example.com")@classmethod / @staticmethodBoth @classmethod @deprecated and @deprecated @classmethod (and the equivalent for @staticmethod) produce a working classmethod(deprecated_wrapper). The descriptor is unwrapped at decoration time, the inner function is deprecated, and the result is re-wrapped. FutureWarning fires at call time in either order; no decoration-time UserWarning is emitted. find_deprecation_wrappers() now inspects descriptor members so wrapped classmethods and staticmethods show up in audit scans.
from deprecate import deprecated
class Repo:
# preferred order
@classmethod
@deprecated(target=new_load, deprecated_in="0.9", remove_in="1.0")
def load(cls, path): ...
# also works in v0.9.0
@deprecated(target=new_load, deprecated_in="0.9", remove_in="1.0")
@classmethod
def load_alt(cls, path): ...generate_deprecation_table() + pydeprecate statusTwo new public enums power the reports: DeprecationStatus (lifecycle classification — ACTIVE, EXPIRED, plus dev/alpha/beta/rc removal windows) and TableStyle (COMPACT / MATRIX). Tables are grouped by module and API type, auto-detect the package version from pyproject.toml or installed metadata, and write to stdout or a file via --output. pydeprecate all now includes the table automatically.
pydeprecate status src/mypackage --style compact --output DEPRECATIONS.mdSample output (COMPACT style):
| Original API | API Type | New API | Deprecated | Remove | Current Status |
|---|---|---|---|---|---|
mypackage.load |
callable | mypackage.load_v2 |
v1.0 | v2.0 | 📢 Deprecation Active |
mypackage.Client.connect |
classmethod | mypackage.Client.open |
v1.1 | v1.4 | ⏰ Removal Imminent |
mypackage.to_dict |
callable | — | v0.9 | v1.3 | 💥 Past Removal Date |
One breaking change: the CLI flag --skip_errors was renamed to --exit-zero across all four subcommands (check, expiry, chains, all). Update any CI scripts before upgrading.
# before — fails on v0.9.0
pydeprecate check src/mypackage --skip_errors
# after
pydeprecate check src/mypackage --exit-zerov0.8.0 deprecations (target=None, target=True, DeprecationWrapperInfo field renames) are still active and will be removed in v1.0. See MIGRATION.md for full before/after examples.
@deprecated. Warning emitted eagerly at call time, before the first next(). All three TargetMode variants work transparently. (#176)async def coroutine wrapper support for @deprecated. Wrapper is async def, inspect.iscoroutinefunction(wrapper) returns True, warning fires when the coroutine is awaited. All three TargetMode variants work with async sources and async targets. (#180)@deprecated. No more decoration-time UserWarning; wrapper is a sync callable that fires the warning eagerly and returns an async-generator object. All three TargetMode variants work. (#181)@classmethod / @staticmethod. Both orders produce classmethod(deprecated_wrapper); decorator order no longer matters. (#178)@deprecated — ARGS_REMAP + NOTIFY combination. Rename arguments first, deprecate the whole function later. Six other stacking shapes emit UserWarning at decoration time and will become TypeError in v1.0. (#172)generate_deprecation_table() + pydeprecate status CLI subcommand. New DeprecationStatus and TableStyle public enums. New --style and --output CLI flags. Integrated into pydeprecate all. Auto-detects package version from pyproject.toml or installed metadata. (#133)ChainType enum now exported as public API. Previously returned by validate_deprecation_chains(); now listed in deprecate.__all__. (#133)find_deprecation_wrappers() now inspects classmethod and staticmethod descriptors on class members. (#178)--skip_errors → --exit-zero across all four subcommands (check, expiry, chains, all). No transitional release — flag was new in v0.8.0. Canonical spelling is --exit-zero; --exit_zero also accepted by the CLI framework. (#187)cmd_check / cmd_expiry / cmd_chains / cmd_all keyword renamed from skip_errors to exit_zero. (#187)@deprecated stacking shapes now emit UserWarning at decoration time naming the specific shape. Scheduled to become TypeError in v1.0. (#172)ARGS_REMAP + NOTIFY is now classified as STACKED rather than TARGET. Fixes audit reports for the supported stacking shape. (#172)deprecate.* warnings only. Third-party warnings emitted during a scan are no longer silenced. (#133)generate_deprecation_table() gains include_members parameter for scanning descriptor members; validate_deprecation_expiry() default unchanged at include_members=False to preserve existing scan scope. (#133)stacklevel attribution on Python 3.12+. inspect.signature(warnings.warn) raises ValueError on 3.12+ (the C builtin lacks an introspectable signature), which caused every @deprecated warning to point to deprecation.py instead of the caller. Replaced the inspect.signature probe with a try/except at call site. (#176)find_deprecation_wrappers() no longer aborts on PEP 702 typing_extensions.deprecated objects. Scanner was matching PEP 702 wrappers (whose __deprecated__ is a string) and raising ValueError. (#178)*args sources now preserves extra positional arguments. Previously, extra positional arguments past the named parameters were silently dropped. (#180)Full changelog: v0.8.0...v0.9.0
Async & callable shapes
@deprecated. Decorating a generator function now emits the deprecation warning eagerly at call time — before the first next() — consistent with regular function behavior. The generator body executes lazily as normal when iterated. All three TargetMode variants (NOTIFY, ARGS_REMAP, callable target) work transparently; no isgeneratorfunction check is required. (#176)async def coroutine wrapper support for @deprecated. Decorating an async def function now produces an async def wrapper — inspect.iscoroutinefunction(wrapper) returns True. All three TargetMode variants (NOTIFY, ARGS_REMAP, callable target) work with async sources and async targets. The deprecation warning fires when the coroutine is awaited, not when the wrapper is called. pytest-asyncio is required in the test suite to run the async integration tests. (#180)@deprecated. Decorating an async def + yield function no longer emits a UserWarning at decoration time. The wrapper is a sync callable that fires the deprecation warning eagerly at call time and returns the async generator object; callers iterate with async for. All three TargetMode variants work. Because the wrapper is sync, inspect.isasyncgenfunction(wrapper) returns False — frameworks that branch on that flag may need a thin async generator passthrough. (#181)@classmethod / @staticmethod. Both @classmethod @deprecated and @deprecated @classmethod (and the equivalent for @staticmethod) now produce classmethod(deprecated_wrapper) — the descriptor is unwrapped at decoration time, the inner function is deprecated, and the result is re-wrapped. FutureWarning fires at call time in either order; no UserWarning is emitted. (#178)Stacking
@deprecated — ARGS_REMAP + NOTIFY combination. Lifecycle pattern: rename arguments first, deprecate the whole function later. The outer ARGS_REMAP remaps kwargs, then the inner NOTIFY warns and runs the source body. Six other stacking shapes (e.g. callable-over-callable, callable-over-ARGS_REMAP) now emit UserWarning at decoration time naming the specific shape and will become TypeError in v1.0. (#172)Audit & CLI tables
generate_deprecation_table() + pydeprecate status CLI subcommand. Renders compact or matrix-style Markdown reports grouped by module and API-type (function, method, classmethod, staticmethod, property, class, instance). Two new public enums: DeprecationStatus (lifecycle classification — active, expired, plus dev/alpha/beta/rc removal windows) and TableStyle (compact / matrix). New --style and --output CLI flags. Integrated into pydeprecate all. Auto-detects package version from pyproject.toml or installed metadata. (#133)ChainType enum now exported as public API. Previously documented and returned by validate_deprecation_chains(); now listed in deprecate.__all__.find_deprecation_wrappers() now inspects classmethod and staticmethod descriptors on class members so @deprecated-wrapped descriptors are found during scans. (#178)CLI
--skip_errors to --exit-zero across all four subcommands (check, expiry, chains, all). (#187) Breaking change for existing scripts — --skip_errors no longer accepted; update calls to --exit-zero. The new name matches the established linter convention (ruff, pylint, shellcheck) and accurately describes the behaviour: exit-code override only, no exception suppression. The canonical spelling is --exit-zero (dash); the CLI framework also accepts --exit_zero (underscore) as an alias.deprecate.* warnings only. Third-party warnings emitted during a scan are no longer silenced. (#133)Stacking
@deprecated stacking shapes (e.g. callable-over-callable, callable-over-ARGS_REMAP) now emit UserWarning at decoration time naming the specific shape. Scheduled to become TypeError in v1.0. The new module-level _V1_BREAK_VERSION = "v1.0" constant centralises the "Will be TypeError in v1.0" wording across these warnings. (#172)Audit tables
ARGS_REMAP + NOTIFY is now classified as STACKED rather than TARGET. Fixes audit reports for the supported new stacking shape. (#172)generate_deprecation_table() gains include_members parameter for scanning descriptor members; validate_deprecation_expiry() default unchanged at include_members=False to preserve existing scan scope. (#133)stacklevel attribution on Python 3.12+. inspect.signature(warnings.warn) raises ValueError on Python 3.12+ because the C builtin lacks an introspectable signature. This caused every @deprecated warning to point to deprecation.py instead of the caller's file. Fixed by replacing the inspect.signature probe with a try/except at call site: stream(msg, stacklevel=N) is tried first; if TypeError is raised (stream does not accept stacklevel), retried as stream(msg). (#176)find_deprecation_wrappers() no longer aborts on PEP 702 typing_extensions.deprecated objects. The scanner previously checked callable(obj) and hasattr(obj, "__deprecated__"), which matched PEP 702 wrappers (whose __deprecated__ is a string), causing validate_deprecation_wrapper to raise ValueError and abort the scan. Replaced with _has_deprecation_meta(obj), which checks isinstance(..., DeprecationConfig). (#178)*args sources now preserves extra positional arguments. Previously, when forwarding from a *args source, extra positional arguments past the named parameters were silently dropped. (#180)releasing 0.9.0.rc1
releasing 0.9.0.rc1
releasing 0.9.0.rc0
releasing 0.9.0.rc0
No breaking changes. All v0.7.x code continues to run. The items below emit deprecation warnings now and will be removed in v1.0.
pyDeprecate 0.8.0 is the TargetMode enum & CLI tooling release — deprecation intent is now typed and explicit, warn-only deprecation is the default, and a new pydeprecate CLI lets you scan any package for misconfigured wrappers without writing a single audit script.
v0.8.0 centers on TargetMode: a proper enum (TargetMode.NOTIFY, TargetMode.ARGS_REMAP) replacing the target=None / target=True boolean sentinels. The old sentinels still work — they emit FutureWarning at decoration time — so you've got a full release cycle to migrate before they're removed in v1.0.
target now defaults to TargetMode.NOTIFY, so the most common pattern — warn callers and run the original body unchanged — needs nothing more than deprecated_in and remove_in:
@deprecated(deprecated_in="1.0", remove_in="2.0")
def old_fn(): ...A new pydeprecate CLI (four subcommands) rounds out the release. Run pydeprecate check path/to/mypackage to surface misconfigured wrappers across a whole codebase in seconds.
TargetMode enumfrom deprecate import deprecated, TargetMode
# Warn-only: source body executes unchanged
@deprecated(target=TargetMode.NOTIFY, deprecated_in="0.8", remove_in="1.0")
def old_api(): ...
# Argument-rename: warns only when old arg name is passed
@deprecated(
target=TargetMode.ARGS_REMAP,
deprecated_in="0.8",
remove_in="1.0",
args_mapping={"old_param": "new_param"},
)
def new_api(new_param): ...TargetMode is exported from deprecate and works everywhere target= is accepted: @deprecated, deprecated_class(), and deprecated_instance().
target is now optional — TargetMode.NOTIFY is the default. Drop the target= entirely:
@deprecated(deprecated_in="0.8", remove_in="1.0")
def old_fn(): ...Omitting deprecated_in now surfaces a UserWarning at decoration time, not silently at call time, so misconfiguration is visible immediately.
pydeprecate CLIpydeprecate check path/to/mypackage # validate wrapper configuration
pydeprecate expiry path/to/mypackage # find wrappers past remove_in date
pydeprecate chains path/to/mypackage # detect deprecated→deprecated chains
pydeprecate all path/to/mypackage # run all three in one passAlso available as python -m deprecate. Requires pip install 'pyDeprecate[cli]' for all subcommands (fire dependency). expiry additionally needs pip install 'pyDeprecate[audit]'. Or install both: pip install 'pyDeprecate[cli,audit]'.
No breaking changes. All v0.7.x code continues to run. The items below emit deprecation warnings now and will be removed in v1.0.
target=None → TargetMode.NOTIFY# before — emits FutureWarning at decoration time
@deprecated(target=None, deprecated_in="0.8", remove_in="1.0")
def old_fn(): ...
# after — explicit or implicit (default)
@deprecated(target=TargetMode.NOTIFY, deprecated_in="0.8", remove_in="1.0")
def old_fn(): ...
# simplest form — NOTIFY is the default
@deprecated(deprecated_in="0.8", remove_in="1.0")
def old_fn(): ...target=True → TargetMode.ARGS_REMAP# before — emits FutureWarning at decoration time
@deprecated(target=True, deprecated_in="0.8", remove_in="1.0", args_mapping={"old_arg": "new_arg"})
def new_fn(new_arg): ...
# after
@deprecated(target=TargetMode.ARGS_REMAP, deprecated_in="0.8", remove_in="1.0", args_mapping={"old_arg": "new_arg"})
def new_fn(new_arg): ...DeprecationWrapperInfo field renames| Old name | New name | Removed in |
|---|---|---|
empty_mapping |
empty_args_mapping |
v1.0 |
identity_mapping |
identity_args_mapping |
v1.0 |
# before
if info.empty_mapping or info.identity_mapping:
...
# after
if info.empty_args_mapping or info.identity_args_mapping:
...DeprecationConfig.target no longer stores raw sentinelsCode that inspects wrapper.__deprecated__.target and compares against None or True must update:
# before
assert fn.__deprecated__.target is None
# after
from deprecate import TargetMode
assert fn.__deprecated__.target is TargetMode.NOTIFYTargetMode enum (NOTIFY, ARGS_REMAP) exported from deprecate — typed replacement for target=None / target=True sentinels. (#150)target defaults to TargetMode.NOTIFY on @deprecated — warn-only deprecation now requires only deprecated_in and remove_in. (#162)pydeprecate CLI — check, expiry, chains, all subcommands.
template_mgs and args_extra on deprecated_class() and deprecated_instance() — proxy factories now at full parity with @deprecated. (#150)DeprecationWrapperInfo.empty_deprecated_in — True when deprecated_in is absent; for CI pipeline use. (#166)DeprecationConfig.misconfigured — True when an invalid raw target sentinel (False) was passed at decoration time; surfaced via DeprecationWrapperInfo.misconfigured_target. (#150)num_warns=0 documented — equivalent to stream=None; suppresses all warnings. (#150)@deprecated(target=callable_a) stacked over another callable-target wrapper now emits UserWarning at decoration time instead of crashing with TypeError at call time. (#169)template_mgs validated at decoration time — malformed %-style placeholders raise ValueError immediately. (#169)DeprecationConfig.target normalized at decoration time — stores TargetMode or Callable, never raw None/True/False. (#150)TargetMode combos warn at construction time — ARGS_REMAP without args_mapping, NOTIFY with args_mapping or args_extra all emit UserWarning immediately. (#150)https://borda.github.io/pyDeprecate/stable/ (and /<tag>/); the bare root redirects to stable/. Existing bookmarks to flat paths will break on first deploy. (#148)check subcommand reports chains as warnings; chains/all subcommands report chains as errors. (#149)target=None — use TargetMode.NOTIFY. Emits FutureWarning. Removed in v1.0. (#150)target=True — use TargetMode.ARGS_REMAP. Emits FutureWarning. Removed in v1.0. (#150)target=False — never valid; now emits UserWarning, treated as TargetMode.NOTIFY. Raises TypeError in v1.0. (#150)DeprecationWrapperInfo.empty_mapping → empty_args_mapping. Emits DeprecationWarning. Removed in v1.0. (#166)DeprecationWrapperInfo.identity_mapping → identity_args_mapping. Emits DeprecationWarning. Removed in v1.0. (#166)@deprecated stacked under @typing.deprecated no longer raises AttributeError on __deprecated__ lookup. (#169)FutureWarning on deprecated_class() in NOTIFY mode. (#162)__qualname__ no longer trigger spurious TypeError at decoration time; the guard still raises for genuine cross-class forwarding. (#169)args_mapping rename no longer clobbers source default when both old and new parameter names are supplied simultaneously. (#150)TargetMode enum, CLI subcommands, proxy parity, cross-class guard hardening, docs site restructureFull changelog: v0.7.0...v0.8.0
Core API & config
TargetMode enum exported from deprecate. TargetMode.NOTIFY replaces target=None and TargetMode.ARGS_REMAP replaces target=True. Both are public API. (#150)args_extra parameter for deprecated_class() and deprecated_instance(). Injects fixed keyword arguments into forwarded calls after args_mapping has been applied, matching the same semantics as @deprecated(args_extra=...). Ignored (with a construction-time UserWarning) when target is TargetMode.NOTIFY. (#150)template_mgs parameter for deprecated_class() and deprecated_instance(). Overrides the built-in warning message template with a %-style format string, matching the same semantics as @deprecated(template_mgs=...). Available placeholders: %(source_name)s, %(deprecated_in)s, %(remove_in)s, %(target_name)s (callable target only), %(target_path)s (callable target only), %(argument_map)s (args_mapping warnings only). (#150)DeprecationConfig.misconfigured field. Boolean field on the shared metadata dataclass; True when an invalid raw target sentinel (False) was passed at decoration time. Audit tools surface this via DeprecationWrapperInfo.misconfigured_target. (#150)template_mgs validated at decoration time. Malformed %-style format strings now raise ValueError immediately at decoration time — not silently at call time. Applies to @deprecated and the deprecated_class()/deprecated_instance() proxy factories. (#169)@deprecated(target=fn_a) on a callable whose target is itself a callable-target @deprecated wrapper now emits UserWarning at decoration time instead of crashing with TypeError at call time. (#169)CLI & audit
pydeprecate CLI command. Run pydeprecate <subcommand> path/to/your/package to scan any package or module for misconfigured @deprecated wrappers — reports invalid argument mappings, identity mappings, and no-effect wrappers with rich-formatted output when rich is available. Also available as python -m deprecate. (#76)check, expiry, chains, all. check validates wrapper configuration; expiry reports wrappers past their remove_in deadline (requires pip install 'pyDeprecate[audit]'); chains detects deprecated-to-deprecated forwarding chains; all runs all three in a single scan pass. Flags: --norecursive, --skip_errors. (#149)DeprecationWrapperInfo.empty_deprecated_in field. True when deprecated_in is absent on a wrapper; intended for CI pipeline introspection. dataclasses.asdict() output and repr() now include this field. (#166)Docs
llms.txt, and git-revision-date-localized plugin. README is unchanged (still the PyPI cover page). (#146)target=None sentinel — use TargetMode.NOTIFY. Passing target=None now emits a FutureWarning at decoration time. The sentinel remains accepted but will be removed in v1.0. Migrate to target=TargetMode.NOTIFY. (#150)target=True sentinel — use TargetMode.ARGS_REMAP. Passing target=True now emits a FutureWarning at decoration time. The sentinel remains accepted but will be removed in v1.0. Migrate to target=TargetMode.ARGS_REMAP. (#150)DeprecationWrapperInfo attributes empty_mapping → empty_args_mapping and identity_mapping → identity_args_mapping. Old names are kept as deprecated @property aliases that emit DeprecationWarning on access and will be removed in v1.0. (#166)Config & API
TargetMode combinations now warn at construction time. TargetMode.ARGS_REMAP without args_mapping, TargetMode.NOTIFY with args_mapping, and TargetMode.NOTIFY with args_extra all surface a UserWarning immediately. (#150)DeprecationConfig.target always stores a normalised TargetMode or callable. Legacy boolean sentinels (True / False) are now normalised at decoration time and are never stored verbatim in DeprecationConfig.target. Code that inspects __deprecated__.target must compare against TargetMode.NOTIFY, TargetMode.ARGS_REMAP, a callable, or None — never against True or False. (#150)deprecated_class() with target=TargetMode.NOTIFY now emits UserWarning at decoration time when args_mapping or args_extra is supplied. These parameters are ignored in NOTIFY mode; passing them has always been a misconfiguration. The warning will become TypeError in v1.0. (#150)target parameter of @deprecated now defaults to TargetMode.NOTIFY. Callers can omit target entirely for warn-only deprecation: @deprecated(deprecated_in="1.0", remove_in="2.0") is now the canonical form. Passing target=TargetMode.NOTIFY explicitly remains valid. This default is permanent and will not change in future releases. (#162)UserWarning when @deprecated omits deprecated_in. When deprecated_in is absent a UserWarning is emitted immediately at decoration time (not at call time) regardless of target shape (TargetMode.NOTIFY, callable, or ARGS_REMAP), even if remove_in is set. Applies to functions and methods (not classes). Suppressed when stream=None or when a custom template_mgs is provided. (#162)CLI
check subcommand reports deprecated-to-deprecated chains as warnings (exit 0); chains and all subcommands report chains as errors (exit 1). (#149)Docs
https://borda.github.io/pyDeprecate/stable/ (for the stable alias) and https://borda.github.io/pyDeprecate/<tag>/ (for release tags). The root URL (https://borda.github.io/pyDeprecate/) redirects to stable/. External bookmarks to flat paths like .../pyDeprecate/troubleshooting.html will break on first deploy — update them to .../pyDeprecate/stable/troubleshooting.html. (#148)Guards & compatibility
@deprecated was stacked under a PEP 702 @typing.deprecated decorator, wrapped_fn attempted to look up __deprecated__ on the outer wrapper and raised AttributeError. Fixed by capturing dep_meta as a closure variable at decoration time instead of re-reading it from the wrapper. (#169)TypeError semantics preserved. Two previously documented "irresolvable" false-positive scenarios are now handled: (1) targets with metaclass/dynamic-class qualnames (e.g. type("Name", bases, ns) or manual fn.__qualname__ = "FakeOwner.method") — guard now skips silently when the named class is absent from the target's module globals; (2) pre-applied decorators that rewrite the source's __qualname__ — guard reads the true enclosing class from the Python class-body frame, which cannot be mutated by user decorators. The guard continues to raise TypeError at decoration time for genuine cross-class forwarding. (#169)target=False sentinel now emits UserWarning at decoration time. target=False was never a valid configuration; previously the behavior was undefined. The sentinel now surfaces a UserWarning immediately and will raise TypeError in v1.0. (#150)Warnings & forwarding
FutureWarning emission on deprecated_class() in NOTIFY mode fixed. Using deprecated_class() with target=TargetMode.NOTIFY triggered two FutureWarning emissions per construction call. (#162)args_mapping rename no longer clobbers source default when both old and new parameter names are present. Previously, calling a deprecated wrapper with the old argument name while the source also accepted the new name could silently overwrite the new-name value. The remapping now correctly renames old=X to new=X without discarding a separately supplied new value. (#150)releasing 0.8.0.rc0
releasing 0.8.0.rc0
MkDocs admonition output. @deprecated now accepts docstring_style="mkdocs" (alias: "markdown"). When update_docstring=True, the deprecation notice is…
Docstring injection
@deprecated now accepts docstring_style="mkdocs" (alias: "markdown"). When update_docstring=True, the deprecation notice is injected as a !!! warning "Deprecated in X" admonition instead of a Sphinx .. deprecated:: directive. Use docstring_style="auto" to detect style automatically from existing docstring content. (#134)update_docstring=True now inserts the deprecation notice before the first section (Args:, Returns:, Parameters, …) rather than appending it at the end. (#134)args_mapping is set and update_docstring=True, each renamed or removed argument is annotated directly in the Args: / :param section of the docstring. (#136)Extensions & demos
deprecate.docstring.griffe_ext, beta) and Sphinx autodoc extension for deprecated classes (deprecate.docstring.sphinx_ext, beta). (#134)getattr/setattr string-literal calls (B009/B010) replaced with direct attribute access. (#139)super().import_object() returns False in the Griffe extension; empty _proxy_doc now delegates to super().get_doc() in the Sphinx extension. (#139)Nothing published for this version
`deprecated_class()` and `deprecated_instance()` — full proxy support. Enum, dataclass, and built-in types can now be wrapped in a transparent proxy.…
deprecated_class() and deprecated_instance() — full proxy support. Enum, dataclass, and built-in types can now be wrapped in a transparent proxy. Attribute access, item access, method calls, and class behaviour all forward to the underlying type with a FutureWarning emitted on first access. (#114)isinstance() / issubclass() semantics on proxy classes. isinstance(x, proxy) and issubclass(Sub, proxy) now work as expected — previously raised TypeError. Type checks do not consume the warning budget. (#126)@deprecated on a class raises TypeError. Applying @deprecated directly to a class now raises TypeError at decoration time instead of silently misbehaving. Superseded in v0.6.0.post0 by a UserWarning + delegation to deprecated_class(). Use @deprecated_class() for class-level deprecation. (#120)Audit API renamed for consistency. Old names remain as @deprecated shims until v1.0. (#125)
| Old name | New name |
|---|---|
find_deprecated_callables |
find_deprecation_wrappers |
validate_deprecated_callable |
validate_deprecation_wrapper |
DeprecatedCallableInfo |
DeprecationWrapperInfo |
no_warning_call renamed to assert_no_warnings. The new name mirrors assertWarns / assertRaises from the standard library, making test intent immediately obvious. Old name kept as a deprecated alias until v1.0. (#131)
target on a non-__init__ method previously silently forwarded self of the wrong type — always a runtime bug, never a valid pattern. The guard now raises TypeError at decoration time so the misconfiguration is caught immediately. (#121)find_deprecation_wrappers() no longer reports false invalid_args for proxy objects. The proxy __call__ catch-all signature previously caused all args_mapping keys to be flagged as invalid; signature validation is now skipped for proxy objects. (#124)Nothing published for this version
`deprecate.audit` module — deprecation lifecycle management. A dedicated module grouping all inspection and enforcement utilities, designed to be call…
deprecate.audit module — deprecation lifecycle management. A dedicated module grouping all inspection and enforcement utilities, designed to be called from pytest or CI scripts. Requires the optional [audit] extra: pip install pyDeprecate[audit]. (#111)find_deprecated_callables() / validate_deprecated_callable() — zero-impact wrapper detection. Scans a module or package for @deprecated wrappers that have no real effect: invalid args_mapping keys, identity mappings, self-referencing targets, or missing version fields. Returns DeprecatedCallableInfo dataclasses. (#72)validate_deprecation_expiry() — enforce removal deadlines in CI. Scans a module or package and returns all wrappers whose remove_in version has been reached or passed. Auto-detects the installed package version. Integrate as a pytest fixture or CI step to prevent zombie code from shipping past its scheduled removal. (#89)validate_deprecation_chains() — detect deprecated-to-deprecated forwarding. Identifies wrappers whose target is itself a deprecated callable, forming chains that users traverse unnecessarily. Reports two chain kinds via the ChainType enum: TARGET (forwarding chain) and STACKED (composed argument mappings). (#90)@deprecated wrappers now correctly handle var-positional Enum signatures. A subtle edge case where callables with var-positional parameters in their Enum signature caused incorrect argument forwarding is now resolved. (#104)Nothing published for this version
`update_docstring` parameter — automatic Sphinx deprecation notices. Set update_docstring=True on @deprecated to automatically append a .. deprecated:…
update_docstring parameter — automatic Sphinx deprecation notices. Set update_docstring=True on @deprecated to automatically append a .. deprecated:: reStructuredText block to the function's docstring. IDE tooltips and Sphinx-generated API docs show the notice without any manual edits. (#31)FutureWarning instead of DeprecationWarning. DeprecationWarning is silenced by Python's default warning filters outside of test contexts, making it invisible to most end-users. FutureWarning is shown by default, ensuring callers actually see the migration message. (#16)Nothing published for this version
…argument names. Extra arguments from the deprecated call are now forwarded correctly.
target functions using **kwargs are now supported. Previously, forwarding to a target that accepted **kwargs and accessed them via kwargs.get(...) raised TypeError for unrecognised argument names. Extra arguments from the deprecated call are now forwarded correctly. (#6)The return type of void() is now properly annotated — IDE and type checker warnings about unused parameters in deprecated function bodies are suppress…
void() type annotation corrected to satisfy mypy. The return type of void() is now properly annotated — IDE and type checker warnings about unused parameters in deprecated function bodies are suppressed correctly.`skip_if` parameter — conditional deprecation. Pass a bool or a zero-argument callable returning bool to skip the warning and forwarding when a runtim…
skip_if parameter — conditional deprecation. Pass a bool or a zero-argument callable returning bool to skip the warning and forwarding when a runtime condition is true. Useful for gating deprecation behaviour on package version checks or feature flags. (#4)`target=True` — self-deprecation mode. Deprecate and remap arguments within the same function without forwarding to a separate callable. Use with args…
target=True — self-deprecation mode. Deprecate and remap arguments within the same function without forwarding to a separate callable. Use with args_mapping to rename a parameter while keeping the function body intact. (#3)void() helper. Accepts any arguments and returns None. Silences IDE "unused parameter" warnings in deprecated function bodies where the body is never reached.no_warning_call() context manager. Assert that a block of code raises no deprecation warning — useful for verifying that new API paths are clean in tests. Renamed to assert_no_warnings() in v0.6.0. (#2)@deprecated decorators. Multiple @deprecated(True, ...) decorators can be stacked on the same function for multi-hop argument migrations across versions, each with independent warning counts and version metadata.`num_warns=-1` — always-on warnings. Setting num_warns to -1 causes the deprecation warning to fire on every call rather than stopping after N times.
num_warns=-1 — always-on warnings. Setting num_warns to -1 causes the deprecation warning to fire on every call rather than stopping after N times.target=None — warn-only mode. The original function body still executes; @deprecated adds only a warning with no call forwarding. Useful when you want to signal deprecation without changing any call behaviour.`@deprecated(target=callable)` decorator. Marks a function as deprecated and automatically forwards all calls — including argument mapping — to a repl…
Core decorator & forwarding
@deprecated(target=callable) decorator. Marks a function as deprecated and automatically forwards all calls — including argument mapping — to a replacement function. The deprecated function body is never executed when target is a callable.args_mapping renames ({"old": "new"}) or drops ({"old": None}) individual arguments during forwarding.args_extra — inject additional kwargs into the target call. Pass a dict of extra keyword arguments to merge into every forwarded call. Useful for providing default values or adapter arguments that the deprecated API never accepted.Warning controls
num_warns). Warnings fire once per function by default; set to any positive integer to limit the total count per function lifetime.template_mgs). Format string with %(source_name)s, %(target_path)s, %(deprecated_in)s, %(remove_in)s, and %(argument_map)s placeholders.stream). Route warnings to logging.warning, warnings.warn, or any callable.Your coding agent can read these notes before it upgrades. Set up the MCP server →