Changelog🔗
[0.15.0] — 2026-11-09 — Scoped fatal deprecations, a dependency-free CLI, & CI integrations🔗
Added🔗
Fatal deprecations
- Scoped fatal deprecations —
as_exceptions()context manager.with deprecate.as_exceptions():makes deprecations fatal for one block and restores the previous setting on exit, including when the block raises, so nested scopes compose;as_exceptions(False)exempts a block from a strict run, while a wrapper's ownas_exception=Truestays fatal. The scope is held in aContextVar, so it is local to the current thread and asyncio task, and the globaldeprecate.AS_EXCEPTIONSis not modified. Also usable as a decorator on synchronous functions;async def, generator and async-generator functions are rejected withTypeErrorat decoration, and a non-boolflag (including a bare@as_exceptions) raisesTypeErrorat the call. (#240, #253)
CLI & CI
- pre-commit hooks. The repository now ships
.pre-commit-hooks.yaml, so other projects can run the CLI as a hook (repo: https://github.com/Borda/pyDeprecate) without wrapping it in arepo: localentry. There is one hook id per subcommand —pydeprecate-check,pydeprecate-policy,pydeprecate-expiry,pydeprecate-chains,pydeprecate-all, pluspydeprecateas an alias ofpydeprecate-check— soargsonly carries flags, plus the package path when it is not undersrc/. The scan imports the audited package, so packages with runtime dependencies should keep a local hook; the audit guide covers both recipes. (#243) - Composite GitHub Action. Run the audit CLI as a single workflow step in the job's own Python, with inputs for the subcommand, package path, extra flags, advisory mode (
exit-zero), installation, and an optional deprecation status table in the job summary. By default, the action installspyDeprecate[cli,audit]from the action ref;install: falsekeeps the version already installed by the job. Set up Python and install the audited package's dependencies before running the action. (#245) - Scan output files.
--report-json FILEon every CLI subcommand. Writes the run's machine-readable record — the deprecation table of the scan that ran, plus every audit gap with its kind, detail and a remedy hint — on exit0or1; a usage error writes none, and a failed write never changes the exit code. The GitHub Action builds its gap annotations and job summary from it, so a single-gate summary now covers exactly the gate's scan. (#245)pydeprecate all --output FILE.allcan now also save the deprecation table it appends, asstatus --outputdoes; a failure to write the file is reported but never changes the exit code. (#245)include_memberson every audit validator and CLI gate.validate_deprecation_chainsandvalidate_mapping_compatibilityacceptinclude_members: bool = True, the switchfind_deprecation_wrappersalready had, andcheck,expiry,policy,chainsandallaccept--no-include-members(previouslystatusonly) to skip class members. (#249, #252)
Changed🔗
Call forwarding & decoration
functools.partial,partialmethodandsingledispatchmethodsources are rejected at decoration.@deprecatedanddeprecated_callable()raiseTypeErrorfor these objects, including a named partial (one given a__name__throughfunctools.update_wrapper) that earlier releases accepted and forwarded with a wrong result. Wrap the object in a plaindefthat calls it and decorate thatdef. (#250)- An empty-string
args_mappingvalue keeps the old argument name under@deprecated.args_mapping={"old_arg": ""}now passesold_argthrough under its original name (the notice still fires), matchingdeprecated_class; onlyNonedrops an argument. On 0.14@deprecateddropped an argument mapped to""like one mapped toNone— map it toNoneif you relied on that. (#251) - Forwarding to a
classmethodaccepts any receiver name. The source no longer needs a first parameter namedcls:klass,mcsor a target receiver such asklsforward the same way, and a source with no leading positional parameter, or a later parameter named like the target's receiver, is rejected withTypeErrorat decoration. A static or plain source that is given a non-class first argument now raisesTypeErrorbefore the warning is emitted or thenum_warnsbudget is spent. (#257) - Generator and async-generator wrappers preserve their callable kind and defer deprecation handling until iteration.
inspect.isgeneratorfunction(wrapper)andinspect.isasyncgenfunction(wrapper)now match the source, so frameworks such as pytest recognise deprecated yield fixtures. Unlike the eager call-time warnings introduced in 0.9.0, the warning,num_warnsbudget,skip_ifevaluation, argument binding and anas_exception/as_exceptions()error now resolve on the firstnext()/__anext__()step; a generator that is never iterated never warns or spends the budget. Movepytest.warns()andpytest.raises()assertions around the first iteration, and keepas_exceptions()active at that step rather than only around generator creation. (#256) - Warnings that reported a
deprecateframe now point at the caller. Call-time warnings already named the user's call site; the remaining ones, notably the inner warning of a stacked deprecation chain, now carry the caller's line and module too. A warning filter keyed onmodule="deprecate"that matched those warnings no longer does, so they surface — and raise under-W error— so key filters on the calling code's module instead. (#250) - A custom
streamis called exactly once. pyDeprecate reads its signature before the call and never retries after aTypeError. A stream that declares**kwargsreceivesstacklevel=and must accept or drop it: forward it towarnings.warnorlogging.warning, not toprint(msg, **kwargs), which raisesTypeError; declaredef sink(msg)to receive the message alone. A bare(*args, **kwargs)functools.wrapspass-through is classified by the callable it wraps, and a stream whose only parameter is namedstacklevelreceives the message alone. (#250) - Multi-argument rename warnings name only the arguments still within their budget. With
num_warns=1, a call usingold_aand then a call usingold_aandold_bno longer re-listsold_ain the second message. (#251) - A class decorated with
@deprecatedgets its dispatch notice once under concurrent decoration. The one-time notice is claimed under a lock, and the set of remembered classes is capped at 4096 keys, evicting the oldest, so an evicted class may warn again. (#251)
Proxies
- Function proxies bind like methods, and only plain functions do. A
deprecated_instanceover a plain function or a@deprecatedwrapper stored as a class attribute passes the instance asself, soModel().scale(2)reaches the function with the instance; usestaticmethod(deprecated_instance(...))for static-style access. Any other callable —functools.partial, a bound method, a builtin or a Cython binding callable — is served unbound and silent on every Python version, so a deprecatedpartialclass attribute no longer warns through Python's own binding. (#250) - Proxies carry only the protocols of the wrapped object or its replacement. A
deprecated_instanceordeprecated_classproxy now allocates a cached subclass with only the optional dunders the served object supports, so a deprecateddictis no longercallable(), anIterator,os.PathLike,AwaitableorAsyncIterable, and a protocol the object disables (__iter__ = None,__contains__ = None) stays disabled instead of falling back. The set is fixed when the proxy is created. Visible differences: a missing-protocol error names_DeprecatedProxy;inandreversed()that fall back through__iter__now warn;type(proxy) is _DeprecatedProxyisFalse, so useisinstance(proxy, DeprecationProxy); and the generated proxy subclasses cannot be pickled by reference. (#257) - When a proxy warns or refuses.
- Fatal mode refuses reads before they run. Under
as_exception=Trueor strict mode, a subscript, iteration,round(), a unary operator or a mapped attribute read raisesDeprecatedErrorbefore touching the wrapped object, so a missing key adds nodefaultdictentry and a mapped getter or user__iter__never runs. (#250) next()warns before advancing. The warning is delivered before the wrapped iterator moves, so aFutureWarningraised as an error, or a failingstream, no longer consumes an item. The call that raisesStopIterationnow counts as a use and spends thenum_warnsbudget when it is the first. In-place operators anddefaultdictsubscripts still warn after they run, so their effect stays if delivery fails. (#250)- Async operations warn when the awaitable is obtained.
__await__and__anext__deliver the warning when they hand out the underlying awaitable, so a later failure or exhaustion insideawaitorasync forstill consumes the warning budget. When the warning itself fails (an error filter, a raisingstream), the coroutine the proxy created is closed instead of being left un-awaited, and an async generator stays usable. (#250) - Context entry unwinds when warning delivery fails. If the warning raises on
with proxy:orasync with proxy:, the acquired sync or async context is exited, with the delivery error preserved even when the exit returns a suppression value. (#250) - Item writes and in-place writes honour
skip_if. Withskip_ifactive,proxy[key] = value,del proxy[key]and in-place operators write to the wrapped object and the read-only guard does not apply; item writes never warn. (#250) %(source_path)sin proxy message templates is module-qualified. Fordeprecated_classanddeprecated_instancea custommessage_templatenow receives<module>.<name>when the proxy wraps a class or a plain function, and<source_path>.<attr>for attribute warnings; the default messages are unchanged. (#251)
CLI
- Argument parsing.
- CLI built on
argparse, withoutfireorrich. Thepydeprecatecommand uses only the standard library, sopip install pyDeprecateis enough to run it; thecliextra is kept, empty, sopip install 'pyDeprecate[cli]'keeps working. Option values are always strings, boolean flags are negated with--no-flag(the older--noflagspelling still works, unlisted in--help), options may be spelled with dashes or underscores but never abbreviated, arguments after--are always operands, andpydeprecate <subcommand> --helplists each subcommand's options. Spellings only Fire accepted now exit2with argparse's message: a quoted literal such as--version '"0.10"', a list literal in--exclude(pass comma-separated patterns), a table in--min-grace(write it inpyproject.toml),--path=pkg, short flags such as-r, an empty--report-json=, and the version given both as the second argument and as--version. (#252) - Stricter validation. A boolean flag now accepts only
true,false,1or0(case-insensitive) in both--flag valueand--flag=valueforms, and any other value exits2; a second positional path forcheck,policyorchains, and private--_*options such as--_advisory, exit2as well. Scanning a directory that holds several top-level packages (for example a repository root withmypkg/andtests/) now fails with a non-zero exit and a "contains multiple packages" error instead of passing silently on an empty scan; src-layout and single-package roots resolve as before. (#243) - CLI exit codes.
checkandallnow exit1on misconfigured targets (target=False,TargetMode.NOTIFYwith anargs_mapping,attrs_mappingorargs_extrait ignores,TargetMode.ARGS_REMAPwithout anargs_mapping) and on inner-order@property @deprecatedwrappers, which they used to pass. Exit3is new:pydeprecate: scan error: …when the scanned package itself cannot be imported or its module-level code callssys.exit(), andpydeprecate: internal error: …for a bug in the CLI; both used to exit1, like findings.--exit-zeromasks neither2nor3, and neither writes a--report-jsonfile. A name that is neither an existing path nor an importable module, a single.pyfile and a directory holding several packages exit2.statusexits2for an invalid--version(was a raw error, exit1) and an unknown--style(was a silent fallback, exit0). The GitHub Action annotates exit3as a scan or internal error. (#252) - Audit gaps on stderr. When part of the audit did not run, the CLI now prints an unwrapped stderr line starting with
pydeprecate gap: <kind>:— onepydeprecate gap: module not audited: <module>: <error>line per submodule that failed to import (printed whatever warnings filters the audited package installs, unlike theaudit: skippedwarning), plusversion not resolved,version not PEP 440,packaging missingandnested files not audited. These lines replace the earlier free-formSkipping …/Could not scan …/ version andpackagingadvisories; tooling reads the same gaps from--report-json. (#245)
Modules, imports & packaging
import deprecateno longer loads the audit tooling. The audit names, including the legacy aliases, resolve lazily on first access, which roughly halves import time, and calls whose warning budget is spent take a cheaper path.deprecate.proxyis now a package; its public import paths and pickle identity are unchanged, butinspect.getsourceon the proxy classes no longer finds their source. (#251)- Star-import surface follows
__all__.deprecate.utils,deprecate.routine,deprecate.messaging,deprecate.module,deprecate.deprecation, thedeprecate.proxypackage with its submodules and thedeprecate.docstringmodules now define__all__, sofrom deprecate.<module> import *pulls in only the listed public names. Import the public API from the top-leveldeprecatepackage. (#253) deprecated_module(stream=None)now warns at the call. Thestreamdefault is a separate sentinel, so omitting it keeps delivering throughwarnings.warnsilently, while an explicitstream=Nonestill delivers throughwarnings.warnbut raises aFutureWarningat thedeprecated_module(...)call: from v1.0 it silences the module warning like every other entry point, and thenum_warnsdefault changes from-1to1. The v0.14.0 docstring ("PassNone(default)") described the old default. (#253)DeprecationWrapperInfo.empty_mappingandidentity_mappingwarn withFutureWarning. The deprecated aliases are wrapped with pyDeprecate's own@deprecated, so reading them emits aFutureWarningon every access instead of aDeprecationWarning; update any filter keyed on the old category. Removal stays at v1.0. (#253)- Package metadata in
pyproject.toml. Static metadata moved fromsetup.pyinto[project]; setuptools stays the build backend andsetup.pyonly renders the PyPI long description. The wheel metadata drops the author email, states the license asLicense-Expression: Apache-2.0with itsLicense-File, and lists the homepage asProject-URL: Homepageinstead ofHome-page. (#252)
Deprecated🔗
DeprecationConfigcall-time cache fields.target_positional_only,target_positional_only_order,source_positional_only,source_positional_only_order,source_var_positional_prefix,target_all_param_names,target_accepts_var_positionalandtarget_accepts_var_keywordare internal and leaveDeprecationConfigin v1.0; read the public fields throughget_deprecation_config(obj). Announced in the docs only; reading them emits no warning. (#253)- Internal helpers with public-looking names.
deprecate.utils.get_func_arguments_types_defaultsand the names indeprecate.docstring.injectare internal (left out of each module's__all__) and gain a leading underscore in v1.0; reading them emits no warning. Useinspect.signatureandupdate_docstring=Trueinstead. (#253)
Fixed🔗
Call forwarding & signatures
- Callable forwarding and proxy edge cases. Calls that need no forwarding reach the source with the caller's own positional and keyword arguments, and warning state is cleaned up if a custom stream raises. A warning that fails to deliver no longer spends the proxy's warn budget, reads and other warning data-use proxy operations evaluate
skip_ifonce (calls and writes may still consult it more than once), and a deprecatedfunctools.partialclass attribute is no longer bound or warned about by Python. Audit tables and--report-jsonnamefunctools.partialand callable-instance targets the way the runtime warning does instead of printing an address-bearing repr. (#250) *argssources.- Explicit new argument name wins. When a source declares
*argsand a call passes both the deprecated and the new name, the new-name value is forwarded whatever the keyword order, as the collision warning says. (#251) args_extrais injected.TargetMode.ARGS_REMAPwithargs_extraon a source declaring*argsnow injectsargs_extrafor a caller who already uses the new argument names. (#251)- Forwarding keeps the target's own signature error, and docless classes stay docless. When forwarded keywords need validating, the original introspection error is raised instead of a misleading keyword error, and a deprecated class without a docstring no longer exposes internal proxy text as its
__doc__. (#251) - Decorated callables can be garbage-collected. A signature cache kept every decorated callable alive; it is gone, and a source default whose
__ne__misbehaves (for example an array) no longer crashes a forwarded call. (#251)
Proxies & concurrency
- Instances of a class deprecated in place pickle and load as the original class.
deprecated_classregisters acopyregreducer sopickle,copyanddeepcopyof an instance no longer fail on the proxy bound to the class name; loading rebuilds the original class without a warning. A class that defines or inherits its own__reduce_ex__, a redirect and a stacked deprecation keep their own reduction, and the class object itself still cannot be pickled. (#257) - One misconfiguration notice under threads. The one-time
UserWarningfor a misconfigured wrapper is claimed under the wrapper's lock, so concurrent first calls emit it exactly once. (#251) - Module redirect guard no longer reports false cycles. Concurrent asyncio tasks keep separate guards, a thread started in a copy of a resolving context no longer mistakes its own lookup of the same name for a cycle, and the guard expires when the original lookup finishes, so later work in a copied context resolves the name normally. A real cycle on the same thread still raises
AttributeError. (#251)
CLI & audit
- Version resolution.
- One resolver for every gate.
expiry,all,statusand the JSON table resolve the version the same way (localpyproject.toml, installed metadata, distribution mapping, then__version__), soallandstatusno longer reportversion not resolvedfor a package that only declares__version__, andvalidate_deprecation_expiry/generate_deprecation_tableresolve packages whose distribution name differs from the import name. (#246) - Standalone
pydeprecate expiry. It retains the top-level package's__version__fallback when project and installed metadata cannot resolve a version, so overdue wrappers in uninstalled checkouts still exit1. A genuinely unresolved version now prints oneversion not resolvedgap instead of two, and an auto-detected version that is not PEP 440 skips the check with a gap (exit0) instead of crashing withValueError, aspydeprecate allalready did. (#245, #246) - Every subcommand scans a plain directory of modules.
expiryandchainsraisedValueErroron a directory of top-level modules thatcheckalready scanned; the scanned package no longer sees the CLI's ownsys.argv, the scan header names the file it imported from, and an import resolved outside the scanned directory is reported as another copy auditedgap. (#248) - Boolean flags before the path. A boolean flag placed before the path (
pydeprecate check --exit-zero src/mypackage) no longer swallows the path, andpolicy --message-required/--no-message-requirednow work before the path too instead of exiting2. (#243, #252) --version 0.10keeps its trailing zero. The CLI read it as the number0.1, soexpiry,allandstatuscompared against the wrong release and could pass overdue wrappers. (#252)- A submodule that calls
sys.exit()no longer ends a recursive audit scan.find_deprecation_wrappersskips it with theaudit: skipped <module>warning, as for any other import failure, and the CLI reports amodule not auditedgap; an unguarded__main__.pyused to end the scan with the module's own exit code. (#252)
[0.14.0] — 2026-10-06 — Fatal deprecations, PEP 702 static-checker support, & module warning budgets🔗
Added🔗
- Opt-in audit discovery for PEP 702-only deprecations.
find_deprecation_wrappers(..., include_pep702=True)lists symbols marked only withwarnings.deprecatedortyping_extensions.deprecated. Each row keeps a shapeapi_type(callable,class,class method,staticmethod, ...) like any other row, and carries the decorator's message in the newDeprecationWrapperInfo.pep702_messagefield, which isNoneon every pyDeprecate row. Only symbols the scanned package defines are listed: no inherited class members, no__new__/__init_subclass__hooks the decorator installs, no instances of a deprecated class, a class bound under a second name lists its PEP 702 member rows once, under the binding named like the class (its pyDeprecate member rows still appear under each name), and no imports from other top-level packages. The default scan and existing audit gates remain unchanged. (#237) - Static callable typing for deprecation decorators.
@deprecatedanddeprecated_callable()now reject non-callable inputs in static checks while preserving supported callable and descriptor types. (#237) - Fatal deprecations —
as_exception=Trueand process-wide strict mode. All deprecation entry points can raiseDeprecatedErrorinstead of warning;deprecate.AS_EXCEPTIONSand the import-timeDEPRECATE_AS_EXCEPTIONSenvironment setting enable strict mode globally. Fatal warning paths ignorenum_warnsandstream=None, respectskip_if, and stop callable execution or forwarding. Proxy operations that never warn remain silent; ordinary descriptor getters can execute before the fatal error.as_exceptionis keyword-only and the last parameter on every public entry point (deprecated,deprecated_callable,deprecated_class,deprecated_instance,deprecated_module), so future additions never shift existing positional usage. (#236) - Module warning budgets —
deprecated_module(..., num_warns=...). Module deprecations can now cap warning delivery across all public attribute accesses:0delivers none, a positiveNdelivers exactlyN, and the default-1preserves unlimited delivery without taking the budget lock. (#233)
Changed🔗
- The package ships type information (
py.typed); decorated callables and classes keep their own static type. Type checkers previously saw every pyDeprecate object asAny. Now@deprecated/deprecated_callable()keep the decorated callable's or descriptor's type, and adeprecated_class(...)call-form alias is typed as the class it forwards to —type[New]fordeprecated_class(target=New, ...)(Cls),type[Cls]when there is no classtarget(the@deprecated_class(...)decorator form stays the class statement in mypy, while pyright applies the decorator's return type, so it istype[New]there when a classtargetis given) — while remaining a proxy at runtime, soisinstance/issubclassagainst the alias, construction, and attribute access type-check. TheOld: DeprecationProxy[New] = deprecated_class(...)(Cls)annotation recipe no longer type-checks;DeprecationProxy[T]stays the annotation fordeprecated_instance()proxies. Call-form aliases now report old names accepted only throughargs_mapping/attrs_mapping(call-arg/attr-defined), as the decorator form already did. (#237)
Fixed🔗
- Setting or deleting PEP 702 metadata on a deprecated proxy stays local to the proxy. Assigning
__deprecated__keeps the marker on the proxy, and deleting it no longer forwards deletion to the wrapped object, including read-only proxies withskip_ifenabled. After deletion, reading__deprecated__raisesAttributeErrorinstead of returning the wrapped object's marker, andcopy.copy,copy.deepcopy, andpicklekeep a PEP 702 message written onto the proxy instead of re-rendering pyDeprecate's own. (#237)
[0.13.0] — 2026-09-18 — PEP 702 metadata, deprecation policy lint, & the coding-agent plugin🔗
Added🔗
- Deprecation policy governance lint —
validate_deprecation_policy()andpydeprecate policy. Checks whether each deprecation was scheduled responsibly against two independently disable-able rules:min_grace(remove_inmust be one clean single-component bump beyonddeprecated_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 strictdeprecate.audit.GraceWindow(count, unit), which may also be passed directly; a mixed bump such as1.2→2.3never passes) andmessage_required(warning must name a replacement). Exportsvalidate_deprecation_policy,PolicyRule,GraceWindow, andVersionBump. Thepydeprecate policyCLI subcommand takes no--version, exits1on violations,2on a malformed--min-grace, and0with an install hint when the[audit]extra (packaging) is absent andmessage_requiredfinds nothing to flag —message_requiredruns withoutpackagingand can still exit1on its own.pydeprecate allruns the check in advisory-only mode — violations are printed as a[WARNING]but never changeall's exit status. The CLI resolves each rule as flag →[tool.pydeprecate.policy]table in the nearestpyproject.toml(keys are the rule slugs;min-grace = { minor = 3 }or a quoted dotted delta;min-grace = falsedisables the window) → built-in default, prints aPolicy:header naming each value's source, exits2on a malformed file value or an unreadablepyproject.toml(inpolicyandallalike), and warns on unknown keys. (#231) excludefor every audit scan.find_deprecation_wrappers(),validate_deprecation_expiry(),validate_deprecation_policy(),validate_deprecation_chains(),validate_mapping_compatibility()andgenerate_deprecation_table()take a keyword-onlyexcludelist offnmatchglobs 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 theexcludelist of[tool.pydeprecate]in the nearestpyproject.toml, prints anExclude:header naming the source, and exits2on a non-string value. The recursive walk replacespkgutil.walk_packages(which imports a package before it can be skipped) with aniter_moduleswalk that checks exclusion first. (#231)- Public
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) pydeprecatecoding-agent plugin for Codex and Claude Code. Ships sharedsunsetandpruneskills underplugins/pydeprecate/, with dual-host manifests and marketplace catalogs (.claude-plugin/,.codex-plugin/), separate from thepyDeprecatepip package. (#232)
Changed🔗
__deprecated__now follows PEP 702 and stores a rendered message string. pyDeprecate's configuration moved to__deprecation_config__; audit and documentation integrations useget_deprecation_config()so legacy wrappers remain discoverable during migration. Griffe'sRuntimeDocstringsextension now scopes its docstring-replacement guard toget_deprecation_config(obj) is not Noneinstead of a bare__deprecated__truthiness check, so a foreign object decorated only withwarnings.deprecated/typing_extensions.deprecatedis no longer mistaken for a pyDeprecate wrapper. (#230)
Fixed🔗
- Custom
message_templateplaceholders that don't apply to the current target mode no longer raiseKeyError. 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)
[0.12.0] — 2026-08-25 — Module deprecation, a strict callable form, & the AUTO front door🔗
Added🔗
Entry points
deprecated_module()— PEP-562-inspired module-level deprecation. Calldeprecated_module(__name__, deprecated_in=..., remove_in=...)once at the bottom of a module to install a__class__reassignment to a wrapper type that emitsFutureWarningon every public attribute access (including real attributes already in__dict__). Three modes: in-place warn (Mode 1), redirect to replacement module with optionalattrs_mapping(Mode 2), and parent alias viadeprecated_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 aUserWarning. (#203)deprecated_callable()— strict callable-only form of@deprecated. Shares every@deprecatedparameter and raisesTypeErrorat decoration time when applied to a class. Accepts functions, methods, lambdas, and descriptors (classmethod/staticmethod/property); parallels thedeprecated_class/deprecated_instance/deprecated_modulefamily and is exported from the package. (#221)TargetMode.AUTO— decoration-time inference default for the@deprecatedfront door.@deprecatednow defaultstarget=TargetMode.AUTO(wasTargetMode.NOTIFY): an omitted target resolves from the rest of the configuration — a non-emptyargs_mappingresolves toTargetMode.ARGS_REMAPon functions and methods (an empty{}counts as no mapping) (previously this fell into theNOTIFYdefault 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.NOTIFYon callables,target=Nonerecorded on class proxies).AUTOis never stored inDeprecationConfig; the strict formsdeprecated_callable()anddeprecated_class()rejecttarget=TargetMode.AUTOwithTypeError. (#222)
Proxies
skip_ifondeprecated_class()anddeprecated_instance(). The proxies gained the conditional-skip option previously available only on the callable decorators: while thebool(or zero-argument callable returning strictbool) evaluatesTrueat access time, the proxy transparently serves the wrapped source — no warning, noattrs_mappingredirect, noargs_mapping/args_extrahandling, no target forwarding, and noread_onlyenforcement. A non-boolreturn raises the sameTypeErroras the callable form. (#222)- Public
DeprecationProxyprotocol for annotating proxies.from deprecate import DeprecationProxyreplaces annotating against the private_DeprecatedProxy. It is@runtime_checkable, soisinstance(obj, DeprecationProxy)works; being a data protocol,issubclass()raisesTypeError.DeprecationProxy[T]is generic in the type produced by calling the proxy —deprecated_classanddeprecated_instancereturn the concrete_DeprecatedProxytype 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) - AST breadcrumbs on every proxy.
deprecated_classanddeprecated_instanceproxies now carry__wrapped__(the source object) and__signature__(the source's signature), soinspect.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 plaindict, C-level types) get__signature__ = Nonerather than an error — wrapping stays infallible. (#226)
Changed🔗
Decoration & dispatch
@deprecatedon a class is now first-class and permanent. Applying@deprecateddirectly to a class dispatches todeprecated_class(full_DeprecatedProxy—isinstance/__class__transparency, forwarding, budget semantics) and emits a one-time (per module-qualified class name, notice only — removed in1.0) informationalUserWarning—`@deprecated` on class `<Name>` now dispatches to `@deprecated_class`.— suppressed withstream=None. This replaces the v0.6 "will become aTypeError" warning; the threatenedTypeErrornever ships.@deprecated_classstays the explicit, preferred form for classes. (#222)- Explicit
TargetMode.NOTIFY+ a mapping stays flagged; auto-resolve applies only to an omittedtarget. Passingtarget=TargetMode.NOTIFYexplicitly together withargs_mapping(callables and proxies) orattrs_mapping(proxies) emitsUserWarningat decoration time (TypeErrorin v1.0); the mode staysNOTIFYand the mapping is inert at runtime, preserved in audit metadata withmisconfigured=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 omittedtarget, which now auto-resolves. Legacy proxy sentinelstarget=True(without a mapping) andtarget=Falsenow resolve to an unset target and follow the same auto-resolve as an omittedtarget; warn-only proxies recordDeprecationConfig.target=None. (#222) - Non-callable sources raise a clear
TypeError. Applying@deprecatedto a plain object, a__call__instance without__name__, orfunctools.partialof a class now raisesTypeErrornamingdeprecated_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-onlyattrs_mappingis not among them and never has been —deprecated(attrs_mapping=...)raisesTypeError(unexpected keyword argument) in v0.11.0 and v0.12.0 alike; usedeprecated_class(attrs_mapping=...)directly.message_templateandskip_ifpassed through@deprecatedon a class are now forwarded to the proxy (message_templatewas previously dropped silently on the class-dispatch path). (#222)@deprecated(target=TargetMode.ATTRS_REMAP)now raisesTypeErrorfor a class source too. The mode needsattrs_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 secondUserWarningnamingattrs_mappingand pre-announcing thisTypeError. The callable path already raised as proxy-only. The error points todeprecated_class(attrs_mapping=...). (#222)
Packaging
- Source builds now require
setuptools>=82. The[build-system] requiresfloor moved from>=70(via>=80in #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)
Deprecated🔗
template_mgsrenamed tomessage_template. The custom-notice parameter on@deprecated,deprecated_callable,deprecated_class, anddeprecated_instancewas a typo (mgsformsg); it is nowmessage_template.template_mgsstays as a deprecated keyword alias that emits aFutureWarningand forwards its value (passing both raisesTypeError); it will be removed inv1.0. Audit code readingDeprecationConfig.template_mgskeeps working through a read-only property alias — the stored field is nowmessage_template. (#223)
[0.11.0] — 2026-07-15 — Operator forwarding, PEP 560 subclassing, & proxy identity fixes🔗
Added🔗
- Deprecated proxies now forward operator and protocol dunders to the wrapped object. Arithmetic (
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 raisingTypeError. Binary operators preserveNotImplementedsemantics, so unsupported-operand errors surface normally. The warn-policy contract is documented in the_DeprecatedProxydocstring: data use warns (within thenum_warnsbudget), cheap probes stay silent. Caveat: in-place operators (+=,-=,*=, …) return the active object's result rather than a re-wrapped proxy — afterx += 1, the namexis rebound to a plainintand all subsequent uses are silent even if the deprecation window has not closed. (#214) - Subclassing a deprecated class alias now works (PEP 560).
class Child(OldName)on adeprecated_classalias previously raised a confusing metaclass arityTypeError;__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 forattrs_mapping-only andargs_mapping-only proxies. (#214)
Changed🔗
Performance
- Call forwarding is ~2.4× faster. Decoration-time-stable signature facts are now precomputed onto
DeprecationConfiginstead of re-derived on every call (uncachedinspect.getfullargspecremoved 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 forisinstancetransparency._DeprecatedProxyexposes 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 viaobj.__class__ is _DeprecatedProxyshould usetype(obj) is _DeprecatedProxyinstead. (#210)- Proxy identity operations now reflect the served (active) object.
repr(),str(),==, andhash()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/issubclasswith an instance proxy as the second argument now raiseTypeError. Using adeprecated_instanceproxy (one wrapping a value rather than a class) as the second argument toisinstance/issubclasspreviously returnedFalsesilently, hiding the misuse; it now raises the sameTypeErrorthe 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 toinclude_members=Falsewhilefind_deprecation_wrappers()defaulted toTrue, so CI gates silently skipped expired deprecated methods, constructors, classmethods, staticmethods, and properties. The flip can only surface additional expired wrappers — passinclude_members=Falseexplicitly to restore the old scope. (#210)- Batch expiry now warns on an unparsable
remove_ininstead of skipping it silently.validate_deprecation_expiry()(and the CLI expiry gate) previously dropped wrappers whoseremove_inversion could not be parsed, leaving them permanently un-expirable with no signal; such wrappers now emit aUserWarningnaming the callable while the rest of the scan continues. (#216)
Fixed🔗
Audit
- Audit scans no longer double-count re-exported wrappers. Recursive
find_deprecation_wrapperspreviously 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) - Audit report formatting no longer triggers chained proxies. Formatting a report for a wrapper whose
targetis itself a deprecated proxy previously emitted a spuriousFutureWarningfrom 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) - Audit scans survive submodules that fail to import.
find_deprecation_wrappers(recursive=True)previously aborted the whole scan when any submodule raised a non-ImportErrorat import time (e.g.RuntimeErrorfrom an optional dependency); such submodules are now skipped with awarnings.warnnaming the module and error. (#210) - Audit tools now surface deprecated private and dunder members.
_scan_classskipped 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) - Audit now detects proxy self-reference. A
deprecated_instance/deprecated_classproxy 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) - Recursive audit scans tolerate foreign objects that raise on attribute access. Probing a scanned object for deprecation metadata used
getattr(..., default), which only suppressesAttributeError; a third-party object whose__getattr__raised something else (e.g. a lazy proxy raisingRuntimeError) aborted the whole scan. Such failures are now treated as "no metadata". (#216)
CLI
allandstatusno longer fail on plain directories. Scanning a directory without__init__.pyexited 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)- Version auto-detection.
- Resolves distributions whose name differs from the import name.
importlib.metadatalookup previously failed for packages like pyDeprecate itself (importdeprecate, distributionpyDeprecate); the import name is now mapped viapackages_distributions(), and expiry prints an explicit note when it runs without a resolved version. (#215) - No longer picks up an unrelated project's
pyproject.toml. When the scan path is an importable module name rather than a filesystem path,expiry/statuspreviously 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) - Unknown or misspelled flags now exit with an error instead of being silently ignored.
sys.exitinside the Fire invocation previously preempted Fire's unconsumed-argument check, so a typo such as--verisonwas dropped and the command ran with defaults; the CLI now lets Fire report the unconsumed flag and exits non-zero. (#210) - A stream whose
encodingisNoneno longer crashes UTF-8 setup._ensure_utf8_streamscalled.lower()on a possibly-Noneencodingattribute, raisingAttributeErrorfor some redirected streams; aNoneencoding is now tolerated. (#216) - An exception carrying no message now exits with a non-blank stderr line. The top-level handler called
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
- Positional-only parameters.
- Sources no longer raise
TypeErroron every call.@deprecatedondef f(a, /, b=2)previously failed at call time in the default notify mode andTargetMode.ARGS_REMAPbecause positional-only arguments were re-passed as keywords; they are now split back out positionally for both sync and async sources. (#210) - Forwarding no longer misbinds values when an earlier parameter is absent. The positional split previously appended present values in declaration order, sliding a later parameter's value into an earlier defaulted slot; gaps now stop the split and conflicting later values raise
TypeError. The same signature-order dispatch replaces thesetattrfallback indeprecated_class(args_mapping=...), which also failed for required positional-only parameters and frozen dataclasses. (#210) - Surplus
*argsare 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)returned1); the positional tail is now forwarded, and incompatible targets raise the curated mappingTypeError. (#210) - Cross-class forwarding guard.
- No spurious
TypeErrorbetween staticmethods. The cross-class guard exists to preventselfcarrying the wrong type, but staticmethods have noself;@deprecated(target=NewCls.compute)on a staticmethod forwarding to another class's staticmethod is now allowed. (#214) - Now fires for descriptor-decorated methods. The guard read the enclosing class name from a fixed stack depth, which the extra frames of
@property/@classmethod/@staticmethodwrapping pushed out of reach — silently disabling the check; a bounded frame walk now locates the class body regardless of descriptor frames. (#216) - Argument validation against a
**kwargs/*argstarget yields the curated message again. The internal signature helper leaked*args/**kwargsnames into caller-argument validation, producing a rawTypeErrorinstead of the "argument not accepted by target" message; the variadic names are now excluded as documented. (#216)
Proxy, decoration & config
num_warnsquota 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 exactlynum_warnswarnings are emitted under concurrency. (#214)- Instantiating an
attrs_mapping-only deprecated class no longer emits a class-level warning.TargetMode.ATTRS_REMAPscopes the deprecation to the listed attributes, yet plain instantiation fired the blanketFutureWarningand consumed the warn budget; construction is now silent and only deprecated-attribute access warns. (#214) - Deprecated proxies now support
copy.copy,copy.deepcopy, andpickle. Copying or pickling anydeprecated_class/deprecated_instanceproxy previously crashed withRecursionError— the_cfgproperty 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_instanceproxies wrapping plain objects (dicts, lists) are fully picklable;deprecated_classproxies may raisePicklingErrorwhen the decorated class name is replaced by the proxy (the common alias pattern), because pickle cannot find the original class by reference. (#212) - Proxy introspection no longer consumes the warning budget.
hasattr()probes on missing attributes,copy.deepcopyprotocol lookups, and dunder access (e.g.__mro__reads by doc tools) previously emitted the deprecation warning and exhausted the defaultnum_warns=1budget before any real usage. Warnings now fire only on successful non-dunder attribute access. (#210) - Bare
@deprecated(no parentheses) now raises a clearTypeErrorwhen the first argument is not callable. Forgetting the call parentheses previously surfaced as a crypticAttributeError: 'int' object has no attribute '__name__'on the first call; the decorator now explains that it must be called with arguments. (#216) - Custom
template_mgswith a bare%-conversion is now rejected at decoration time. A template containing%s/%d(rather than a%(name)smapping key) silently rendered the whole substitution dict into the warning; such templates now raiseValueErrorwhen the decorator is applied. (#216) args_mapping,args_extra, andattrs_mappingare 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)- A warning stream that raises
TypeErrorinternally is no longer invoked twice. Both the decorator and proxy warning paths called the stream withstackleveland caughtTypeErrorto retry without it — which also swallowed aTypeErrorraised 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 acceptsstacklevel. (#216) - Version normalization preserves PEP 440 local segments.
_normalize_version_stringran its pre/post/dev label rule over the whole string, mangling a legitimate local version such as1.2.3+cudainto1.2.3+cuda0; the local segment (after+) is now split off and re-attached verbatim, and only a single leadingv/Vis stripped. (#216)
[0.10.1] — 2026-07-03 — Cycle detection, strict property, & inner-order audit flag🔗
Added🔗
DeprecationWrapperInfo.inner_order_propertyflag.find_deprecation_wrappers()now setsinner_order_property=Truewhen a plainproperty(not_DeprecatedProperty) has a@deprecated-wrappedfget— the inner-order@property @deprecatedshape where only the getter warns; setters and deleters added via@value.setter/@value.deleterremain silently unprotected. CI pipelines can filter on this field to reject the pattern. (#201)- Opt-in strict
propertyreplacement.from deprecate import propertynow exports_StrictProperty, apropertysubclass that raisesTypeErrorat class-definition time when handed an already-@deprecatedgetter (inner-order detection). Import it in modules that want compile-time enforcement; star imports (from deprecate import *) are unaffected. (#201)
Fixed🔗
- Circular deprecation chains now raise
RuntimeErrorat call time. Callabletargetchains that form a cycle (A → B → A) previously caused unbounded recursion and aRecursionError. The decorator now detects the cycle via aContextVarre-entrancy guard and raises a clearRuntimeErrornaming the circular path. Async deprecation cycle detection was also improved to avoid false positives from concurrent tasks sharing a threading-local guard. (#200) _StrictPropertyTypeErrormessage now references the correct module path. The error previously pointed todeprecate._StrictProperty; corrected todeprecate.deprecation._StrictProperty, which is the actual import path. (#201)
[0.10.0] — 2026-06-21 — Property accessors, class attribute mapping & descriptor targets🔗
Added🔗
Class attribute & dataclass mapping
deprecated_class(attrs_mapping={...})for selective attribute deprecation. Deprecated attribute names emitFutureWarningon read, write, and delete with per-attribute warning budgets.Noneas the redirect value means warn-only (no rename).TargetMode.ATTRS_REMAPis the corresponding mode — can be combined with a callabletargetto redirect attribute access across class boundaries. Multi-hop chains and fan-in renames allowed; cycles raiseValueErrorat decoration time. (#191)- Dataclass
attrs_mappingauto-expand. When the wrapped class is a@dataclass, a singledeprecated_class(attrs_mapping={"old_field": "new_field"})call automatically generates the correspondingargs_mappingentry so both attribute access (obj.old_field) and constructor kwargs (DC(old_field=5)) emitFutureWarningfrom one decorator. Explicitly-providedargs_mappingkeys always win over auto-expanded entries. For non-dataclass targets,attrs_mappingcovers attribute access only. (#193)
Descriptors & property accessors
target=now accepts rawstaticmethod/classmethoddescriptors directly. Inside a class body the new method is still a raw descriptor (not yet bound); passing it astarget=new_methodno longer requires the explicit.__func__suffix._normalize_targetunwraps the descriptor automatically. Forclassmethoddescriptors the symmetric same-class pattern is supported (both deprecated and replacement are classmethods); asymmetric usage raisesTypeErrorat decoration time. (#192)@deprecated @propertynow wrapsfsetandfdelwithFutureWarning. Applying@deprecatedon the outside of@property(outer order, or explicitdeprecated(...)(property(fget, fset, fdel))) now wraps all three accessors. Previously, onlyfgetemitted a warning;fsetandfdelwere silently passed through. Consumers runningfilterwarnings=error::FutureWarningthat wrote to or deleted a deprecated property will now seeFutureWarningerrors — use inner-order (@property @deprecated) or decorate onlyfgetdirectly if you want a silent setter/deleter. Chain-style rebinding via@value.setter/@value.deleteris fully supported through the new_DeprecatedPropertysubclass. (#190)
Stacking & audit
deprecated_classstacking is now supported. Two@deprecated_classdecorators applied to the same class (each with its ownattrs_mappingand 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_DeprecatedProxywithout acast. 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_REMAPnow 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 outerATTRS_REMAPproxy delegates__call__to the innerARGS_REMAPproxy without firing a second global warning. (#193)validate_mapping_compatibility()audit function. Returnslist[DeprecationWrapperInfo]for alldeprecated_classproxies whoseargs_mappingremaps deprecated names toPOSITIONAL_ONLYconstructor parameters — those proxies fall back tosetattrat call time instead of forwarding via kwargs. Use in CI to detect configurations that silently degrade to attribute assignment. (#193)
Fixed🔗
@deprecatednow correctly forwards calls to targets with POSITIONAL_ONLY parameters. When a callabletargetdeclares any parameter as positional-only (def new_fn(x, /): ...), the decorator previously raisedTypeErrorat call time because all arguments were forwarded as kwargs. The decorator now detects POSITIONAL_ONLY params at decoration time, emits aUserWarningnaming the affected parameters, and splits the call-time dispatch so those values are forwarded positionally.args_mappingremaps 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)astarget) remains valid and suppresses theUserWarning. (#194)args_mappingprecedence: 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@deprecatedwithtarget=TargetMode.ARGS_REMAPor a callable target, anddeprecated_class()withargs_mapping. (#198)
[0.9.0] — 2026-06-05 — Generators, async, & markdown audit tables🔗
Added🔗
Async & callable shapes
- Generator function support for
@deprecated. Decorating a generator function now emits the deprecation warning eagerly at call time — before the firstnext()— consistent with regular function behavior. The generator body executes lazily as normal when iterated. All threeTargetModevariants (NOTIFY,ARGS_REMAP, callable target) work transparently; noisgeneratorfunctioncheck is required. (#176) async defcoroutine wrapper support for@deprecated. Decorating anasync deffunction now produces anasync defwrapper —inspect.iscoroutinefunction(wrapper)returnsTrue. All threeTargetModevariants (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-asynciois required in the test suite to run the async integration tests. (#180)- Async generator function support for
@deprecated. Decorating anasync def+yieldfunction no longer emits aUserWarningat 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 withasync for. All threeTargetModevariants work. Because the wrapper is sync,inspect.isasyncgenfunction(wrapper)returnsFalse— frameworks that branch on that flag may need a thin async generator passthrough. (#181) - Order-agnostic
@classmethod/@staticmethod. Both@classmethod @deprecatedand@deprecated @classmethod(and the equivalent for@staticmethod) now produceclassmethod(deprecated_wrapper)— the descriptor is unwrapped at decoration time, the inner function is deprecated, and the result is re-wrapped.FutureWarningfires at call time in either order; noUserWarningis emitted. (#178)
Stacking
- Stacked
@deprecated—ARGS_REMAP + NOTIFYcombination. Lifecycle pattern: rename arguments first, deprecate the whole function later. The outerARGS_REMAPremaps kwargs, then the innerNOTIFYwarns and runs the source body. Six other stacking shapes (e.g. callable-over-callable, callable-over-ARGS_REMAP) now emitUserWarningat decoration time naming the specific shape and will becomeTypeErrorin v1.0. (#172)
Audit & CLI tables
- Markdown deprecation tables —
generate_deprecation_table()+pydeprecate statusCLI 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) andTableStyle(compact/matrix). New--styleand--outputCLI flags. Integrated intopydeprecate all. Auto-detects package version frompyproject.tomlor installed metadata. (#133) ChainTypeenum now exported as public API. Previously documented and returned byvalidate_deprecation_chains(); now listed indeprecate.__all__.- Audit discovery extended to class descriptors.
find_deprecation_wrappers()now inspectsclassmethodandstaticmethoddescriptors on class members so@deprecated-wrapped descriptors are found during scans. (#178)
Changed🔗
CLI
- Renamed
--skip_errorsto--exit-zeroacross all four subcommands (check,expiry,chains,all). (#187) Breaking change for existing scripts —--skip_errorsno 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. - CLI warning suppression narrowed to
deprecate.*warnings only. Third-party warnings emitted during a scan are no longer silenced. (#133)
Stacking
- Misconfigured stacking combinations warn at decoration time. Six previously-undefined
@deprecatedstacking shapes (e.g. callable-over-callable, callable-over-ARGS_REMAP) now emitUserWarningat decoration time naming the specific shape. Scheduled to becomeTypeErrorin 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
- Audit chain classification —
ARGS_REMAP + NOTIFYis now classified asSTACKEDrather thanTARGET. Fixes audit reports for the supported new stacking shape. (#172) generate_deprecation_table()gainsinclude_membersparameter for scanning descriptor members;validate_deprecation_expiry()default unchanged atinclude_members=Falseto preserve existing scan scope. (#133)
Fixed🔗
stacklevelattribution on Python 3.12+.inspect.signature(warnings.warn)raisesValueErroron Python 3.12+ because the C builtin lacks an introspectable signature. This caused every@deprecatedwarning to point todeprecation.pyinstead of the caller's file. Fixed by replacing theinspect.signatureprobe with a try/except at call site:stream(msg, stacklevel=N)is tried first; ifTypeErroris raised (stream does not acceptstacklevel), retried asstream(msg). (#176)find_deprecation_wrappers()no longer aborts on PEP 702typing_extensions.deprecatedobjects. The scanner previously checkedcallable(obj) and hasattr(obj, "__deprecated__"), which matched PEP 702 wrappers (whose__deprecated__is a string), causingvalidate_deprecation_wrapperto raiseValueErrorand abort the scan. Replaced with_has_deprecation_meta(obj), which checksisinstance(..., DeprecationConfig). (#178)- Short-circuit forwarding for
*argssources now preserves extra positional arguments. Previously, when forwarding from a*argssource, extra positional arguments past the named parameters were silently dropped. (#180)
[0.8.0] — 2026-05-21 — Default TargetMode enum & CLI audit tools🔗
Added🔗
Core API & config
TargetModeenum exported fromdeprecate.TargetMode.NOTIFYreplacestarget=NoneandTargetMode.ARGS_REMAPreplacestarget=True. Both are public API. (#150)args_extraparameter fordeprecated_class()anddeprecated_instance(). Injects fixed keyword arguments into forwarded calls afterargs_mappinghas been applied, matching the same semantics as@deprecated(args_extra=...). Ignored (with a construction-timeUserWarning) whentargetisTargetMode.NOTIFY. (#150)template_mgs.- Parameter for
deprecated_class()anddeprecated_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_mappingwarnings only). (#150) - Validated at decoration time. Malformed
%-style format strings now raiseValueErrorimmediately at decoration time — not silently at call time. Applies to@deprecatedand thedeprecated_class()/deprecated_instance()proxy factories. (#169) DeprecationConfig.misconfiguredfield. Boolean field on the shared metadata dataclass;Truewhen an invalid raw target sentinel (False) was passed at decoration time. Audit tools surface this viaDeprecationWrapperInfo.misconfigured_target. (#150)- Stacked-callable-target guard. Applying
@deprecated(target=fn_a)on a callable whose target is itself a callable-target@deprecatedwrapper now emitsUserWarningat decoration time instead of crashing withTypeErrorat call time. (#169)
CLI & audit
pydeprecateCLI command. Runpydeprecate <subcommand> path/to/your/packageto scan any package or module for misconfigured@deprecatedwrappers — reports invalid argument mappings, identity mappings, and no-effect wrappers with rich-formatted output whenrichis available. Also available aspython -m deprecate. (#76)- Four CLI subcommands:
check,expiry,chains,all.checkvalidates wrapper configuration;expiryreports wrappers past theirremove_indeadline (requirespip install 'pyDeprecate[audit]');chainsdetects deprecated-to-deprecated forwarding chains;allruns all three in a single scan pass. Flags:--norecursive,--skip_errors. (#149) - New
DeprecationWrapperInfo.empty_deprecated_infield.Truewhendeprecated_inis absent on a wrapper; intended for CI pipeline introspection.dataclasses.asdict()output andrepr()now include this field. (#166)
Docs
- Multi-page topic documentation site. Replaced the monolithic README-copy home page with a curated 7-page MkDocs Material site: Home, Getting Started, User Guide (Use Cases / void() Helper / Audit Tools), Troubleshooting, and demo links. Switched theme to Material, added Open Graph tags, JSON-LD structured data (SoftwareApplication / FAQPage / TechArticle per page), spec-compliant
llms.txt, andgit-revision-date-localizedplugin. README is unchanged (still the PyPI cover page). (#146)
Deprecated🔗
- Legacy
targetsentinels. target=None— useTargetMode.NOTIFY. Passingtarget=Nonenow emits aFutureWarningat decoration time. The sentinel remains accepted but will be removed in v1.0. Migrate totarget=TargetMode.NOTIFY. (#150)target=True— useTargetMode.ARGS_REMAP. Passingtarget=Truenow emits aFutureWarningat decoration time. The sentinel remains accepted but will be removed in v1.0. Migrate totarget=TargetMode.ARGS_REMAP. (#150)DeprecationWrapperInfoattributesempty_mapping→empty_args_mappingandidentity_mapping→identity_args_mapping. Old names are kept as deprecated@propertyaliases that emitDeprecationWarningon access and will be removed in v1.0. (#166)
Changed🔗
Config & API
- Misconfiguration warnings.
TargetModecombinations warn at construction time.TargetMode.ARGS_REMAPwithoutargs_mapping,TargetMode.NOTIFYwithargs_mapping, andTargetMode.NOTIFYwithargs_extraall surface aUserWarningimmediately. (#150)deprecated_class()withTargetMode.NOTIFYwarns at decoration time whenargs_mappingorargs_extrais supplied. These parameters are ignored inNOTIFYmode; passing them has always been a misconfiguration. The warning will becomeTypeErrorin v1.0. (#150)DeprecationConfig.targetalways stores a normalisedTargetModeor callable. Legacy boolean sentinels (True/False) are now normalised at decoration time and are never stored verbatim inDeprecationConfig.target. Code that inspects__deprecated__.targetmust compare againstTargetMode.NOTIFY,TargetMode.ARGS_REMAP, a callable, orNone— never againstTrueorFalse. (#150)targetparameter of@deprecatednow defaults toTargetMode.NOTIFY. Callers can omittargetentirely for warn-only deprecation:@deprecated(deprecated_in="1.0", remove_in="2.0")is now the canonical form. Passingtarget=TargetMode.NOTIFYexplicitly remains valid. This default is permanent and will not change in future releases. (#162)- Decoration-time
UserWarningwhen@deprecatedomitsdeprecated_in. Whendeprecated_inis absent aUserWarningis emitted immediately at decoration time (not at call time) regardless oftargetshape (TargetMode.NOTIFY, callable, orARGS_REMAP), even ifremove_inis set. Applies to functions and methods (not classes). Suppressed whenstream=Noneor when a customtemplate_mgsis provided. (#162)
CLI
- CLI chains reporting split.
checksubcommand reports deprecated-to-deprecated chains as warnings (exit 0);chainsandallsubcommands report chains as errors (exit 1). (#149)
Docs
- Docs site URL layout is now versioned. Content is published under
https://borda.github.io/pyDeprecate/stable/(for the stable alias) andhttps://borda.github.io/pyDeprecate/<tag>/(for release tags). The root URL (https://borda.github.io/pyDeprecate/) redirects tostable/. External bookmarks to flat paths like.../pyDeprecate/troubleshooting.htmlwill break on first deploy — update them to.../pyDeprecate/stable/troubleshooting.html. (#148)
Fixed🔗
Guards & compatibility
- PEP 702 compatibility crash fixed. When
@deprecatedwas stacked under a PEP 702@typing.deprecateddecorator,wrapped_fnattempted to look up__deprecated__on the outer wrapper and raisedAttributeError. Fixed by capturingdep_metaas a closure variable at decoration time instead of re-reading it from the wrapper. (#169) - Cross-class guard false positives resolved;
TypeErrorsemantics 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 manualfn.__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 raiseTypeErrorat decoration time for genuine cross-class forwarding. (#169) target=Falsesentinel now emitsUserWarningat decoration time.target=Falsewas never a valid configuration; previously the behavior was undefined. The sentinel now surfaces aUserWarningimmediately and will raiseTypeErrorin v1.0. (#150)
Warnings & forwarding
- Double
FutureWarningemission ondeprecated_class()in NOTIFY mode fixed. Usingdeprecated_class()withtarget=TargetMode.NOTIFYtriggered twoFutureWarningemissions per construction call. (#162) args_mappingrename 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 renamesold=Xtonew=Xwithout discarding a separately suppliednewvalue. (#150)
[0.7.0] — 2026-03-31 — Docstring Tooling🔗
Added🔗
Docstring injection
- MkDocs admonition output.
@deprecatednow acceptsdocstring_style="mkdocs"(alias:"markdown"). Whenupdate_docstring=True, the deprecation notice is injected as a!!! warning "Deprecated in X"admonition instead of a Sphinx.. deprecated::directive. Usedocstring_style="auto"to detect style automatically from existing docstring content. (#134) - Google / NumPy section-aware docstring injection.
update_docstring=Truenow inserts the deprecation notice before the first section (Args:,Returns:,Parameters, …) rather than appending it at the end. (#134) - Inline arg deprecation in docstrings. When
args_mappingis set andupdate_docstring=True, each renamed or removed argument is annotated directly in theArgs:/:paramsection of the docstring. (#136)
Extensions & demos
- Griffe extension for mkdocstrings (
deprecate.docstring.griffe_ext, beta) and Sphinx autodoc extension for deprecated classes (deprecate.docstring.sphinx_ext, beta). (#134) - Live demo documentation published to GitHub Pages — MkDocs demo, Sphinx demo, and portal landing page. (#134, #137)
Fixed🔗
- Fixed
getattr/setattrstring-literal calls (B009/B010) replaced with direct attribute access. (#139) - Fixed proxy swap skipped correctly when
super().import_object()returnsFalsein the Griffe extension; empty_proxy_docnow delegates tosuper().get_doc()in the Sphinx extension. (#139)
[0.6.0.post0] — 2026-03-14 — Deprecation Proxy for class/instances🔗
Changed🔗
- Softer class-deprecation fallback.
@deprecatedapplied directly to a class (Enum, dataclass, or plain class) now emits aUserWarningat decoration time and delegates todeprecated_class()internally, instead of raisingTypeError. Code using the old pattern continues to work; the warning points to the recommended API. (#132)
[0.6.0] — 2026-03-13🔗
Added🔗
deprecated_class()anddeprecated_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 aFutureWarningemitted on first access. (#114)- Correct
isinstance()/issubclass()semantics on proxy classes.isinstance(x, proxy)andissubclass(Sub, proxy)now work as expected — previously raisedTypeError. Type checks do not consume the warning budget. (#126)
Changed🔗
@deprecatedon a class raisesTypeError. Applying@deprecateddirectly to a class now raisesTypeErrorat decoration time instead of silently misbehaving. Superseded inv0.6.0.post0by aUserWarning+ delegation todeprecated_class(). Use@deprecated_class()for class-level deprecation. (#120)
Deprecated🔗
- Audit API renamed for consistency. Old names remain as
@deprecatedshims until v1.0. (#125)
| Old name | New name |
|---|---|
find_deprecated_callables |
find_deprecation_wrappers |
validate_deprecated_callable |
validate_deprecation_wrapper |
DeprecatedCallableInfo |
DeprecationWrapperInfo |
no_warning_callrenamed toassert_no_warnings. The new name mirrorsassertWarns/assertRaisesfrom the standard library, making test intent immediately obvious. Old name kept as a deprecated alias until v1.0. (#131)
Fixed🔗
- Cross-class method forwarding now fails at decoration time. Passing a class as
targeton a non-__init__method previously silently forwardedselfof the wrong type — always a runtime bug, never a valid pattern. The guard now raisesTypeErrorat decoration time so the misconfiguration is caught immediately. (#121) find_deprecation_wrappers()no longer reports falseinvalid_argsfor proxy objects. The proxy__call__catch-all signature previously caused allargs_mappingkeys to be flagged as invalid; signature validation is now skipped for proxy objects. (#124)
[0.5.0] — 2026-02-23 — Deprecation Lifecycle Management🔗
Added🔗
deprecate.auditmodule — 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@deprecatedwrappers that have no real effect: invalidargs_mappingkeys, identity mappings, self-referencing targets, or missing version fields. ReturnsDeprecatedCallableInfodataclasses. (#72)validate_deprecation_expiry()— enforce removal deadlines in CI. Scans a module or package and returns all wrappers whoseremove_inversion 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 whosetargetis itself a deprecated callable, forming chains that users traverse unnecessarily. Reports two chain kinds via theChainTypeenum:TARGET(forwarding chain) andSTACKED(composed argument mappings). (#90)
Fixed🔗
@deprecatedwrappers 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)
[0.4.0] — 2025-12-03 — Enhanced Documentation & Modernization🔗
Added🔗
update_docstringparameter — automatic Sphinx deprecation notices. Setupdate_docstring=Trueon@deprecatedto 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)
Changed🔗
- Deprecation warnings now use
FutureWarninginstead ofDeprecationWarning.DeprecationWarningis silenced by Python's default warning filters outside of test contexts, making it invisible to most end-users.FutureWarningis shown by default, ensuring callers actually see the migration message. (#16) - Minimum Python version raised to 3.9. Python 3.8 reached end-of-life in October 2024. (#73)
- License changed from MIT to Apache-2.0.
- Error messages now include the originating class or function name for easier debugging when a mapping fails. (#11)
[0.3.2] — 2021-06-11 — Support containing kwargs in target function🔗
Added🔗
targetfunctions using**kwargsare now supported. Previously, forwarding to a target that accepted**kwargsand accessed them viakwargs.get(...)raisedTypeErrorfor unrecognised argument names. Extra arguments from the deprecated call are now forwarded correctly. (#6)
[0.3.1] — 2021-05-31 — Fixed void typing🔗
Fixed🔗
void()type annotation corrected to satisfy mypy. The return type ofvoid()is now properly annotated — IDE and type checker warnings about unused parameters in deprecated function bodies are suppressed correctly.
[0.3.0] — 2021-04-21 — Conditional skip🔗
Added🔗
skip_ifparameter — conditional deprecation. Pass aboolor a zero-argument callable returningboolto skip the warning and forwarding when a runtime condition is true. Useful for gating deprecation behaviour on package version checks or feature flags. (#4)
[0.2.0] — 2021-03-29 — Improved self arg deprecations🔗
Added🔗
target=True— self-deprecation mode. Deprecate and remap arguments within the same function without forwarding to a separate callable. Use withargs_mappingto rename a parameter while keeping the function body intact. (#3)void()helper. Accepts any arguments and returnsNone. 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 toassert_no_warnings()in v0.6.0. (#2)- Stacked
@deprecateddecorators. 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.
[0.1.1] — 2021-03-21 — Allow infinite warning🔗
Added🔗
num_warns=-1— always-on warnings. Settingnum_warnsto-1causes 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;@deprecatedadds only a warning with no call forwarding. Useful when you want to signal deprecation without changing any call behaviour.
[0.1.0] — 2021-03-20 — Initial release🔗
Added🔗
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 whentargetis a callable.- Automatic argument mapping. Positional arguments are resolved to keyword arguments and forwarded to the target's signature.
args_mappingrenames ({"old": "new"}) or drops ({"old": None}) individual arguments during forwarding. args_extra— inject additional kwargs into the target call. Pass adictof 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
- Configurable warning count (
num_warns). Warnings fire once per function by default; set to any positive integer to limit the total count per function lifetime. - Custom warning message template (
template_mgs). Format string with%(source_name)s,%(target_path)s,%(deprecated_in)s,%(remove_in)s, and%(argument_map)splaceholders. - Custom warning stream (
stream). Route warnings tologging.warning,warnings.warn, or any callable.