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.
✨ Spotlights
Operators and protocol methods now forward to the wrapped object
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 onceSubclassing a deprecated class alias now works
class 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 default
The 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.
Proxy identity fixes: __class__, isinstance/issubclass
A 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.
Proxies support copy, deepcopy, and pickle
These 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.
🔄 Migration guide
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 — passinclude_members=Falseto restore the old scope.- A deprecated proxy's
__class__now reports the wrapped type. If you're detecting a proxy by checkingobj.__class__ is _DeprecatedProxy, switch totype(obj) is _DeprecatedProxyinstead. isinstance/issubclasswith an instance proxy as the second argument now raisesTypeErrorinstead of silently returningFalse.
📝 Notable changes
🚀 Added
- Deprecated proxies forward operators and protocol methods to the wrapped object — arithmetic, comparison/ordering, context managers, iteration, numeric conversion,
os.fspath,format, and async protocols now delegate instead of raisingTypeError; binary operators still returnNotImplementedwhere Python expects it, and in-place operators (+=) rebind to the plain result. (#214) - Subclassing a deprecated class alias now works — the alias resolves to the active class and warns, staying silent when the proxy only remaps attributes or arguments. (#214)
🌱 Changed
- Call forwarding is about 2.4× faster — no behavior change, just less overhead on every call. (#214)
validate_deprecation_expiry()defaultinclude_membersflipped fromFalsetoTrue— your CI expiry gate now scans class members by default; passinclude_members=Falseto restore the old scope. (#210)- A deprecated proxy's
__class__now reports the wrapped object's type — this gives you realisinstancetransparency for JSON encoders, validators,functools.singledispatch, and similar;type(proxy)still tells you it's a proxy. (#210) - Proxy identity operations (
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/issubclasswith an instance proxy as the second argument now raiseTypeError— previously this returnedFalsesilently, which could hide bugs. Class-alias proxies are unaffected. (#216)- Batch expiry checks now warn on an unparsable
remove_ininstead of skipping it silently. (#216)
🔧 Fixed
- Inspecting a deprecated proxy —
hasattr,copy/deepcopy, reading dunder attributes — no longer counts against your warning limit. (#210) - Functions with positional-only parameters no longer raise
TypeErroron every call in notify-only and self-remapping modes (both sync and async). (#210) - Extra
*argsare now forwarded to callable targets instead of silently dropped. (#210) - Positional-only forwarding no longer misbinds arguments when an earlier parameter is missing; the same fix applies to
deprecated_class(args_mapping=...). (#210) - CLI: unknown or misspelled flags now exit non-zero instead of being silently ignored. (#210)
- CLI: version auto-detection no longer walks up into an unrelated project's
pyproject.toml. (#210) - Recursive audits (
find_deprecation_wrappers(recursive=True)) now survive submodules that raise errors other thanImportErroron import. (#210) - Deprecated proxies now support
copy.copy,copy.deepcopy, andpickle. (#212) - The warning count (
num_warns) is now tracked safely when multiple threads call a deprecated function for the first time at once. (#214) - Forwarding between static methods on different classes no longer raises a spurious
TypeError. (#214) - Instantiating a deprecated class that only remaps attributes is now silent — only the deprecated-attribute access itself warns. (#214)
- Audit scans no longer double-count re-exported wrappers. (#215)
- Audit report formatting no longer triggers chained proxies (which caused spurious warnings, drained the warning budget, and fabricated a module path). (#215)
- CLI
all/statusno longer fail on plain directories that lack an__init__.py. (#215) - CLI version auto-detection now resolves distributions whose name differs from the import name. (#215)
- A bare
@deprecated(missing parens) now raises a clearTypeErrorwhen the first argument isn't callable. (#216) - A custom
template_mgswith a bare%-conversion is now rejected withValueErrorat decoration time instead of failing later. (#216) args_mapping,args_extra, andattrs_mappingare now copied defensively when you apply the decorator, so later mutation of the originals can't leak through. (#216)- The cross-class forwarding guard now also catches
@property/@classmethod/@staticmethod-decorated methods. (#216) - A warning
streamthat raisesTypeErrorinternally is no longer invoked twice. (#216) - Argument validation against a
**kwargs/*argstarget gives you the curated error message again. (#216) - Audits now surface deprecated private (
_-prefixed) and dunder members instead of skipping them. (#216) - Audits now detect a proxy pointing at itself and report it as a no-op instead of as an effective deprecation. (#216)
- Recursive audits now tolerate foreign objects whose attribute access raises something other than
AttributeError. (#216) - Version strings with PEP 440 local segments (e.g.
1.2.3+cuda) now parse correctly instead of losing that segment. (#216) - CLI: no longer crashes when the terminal doesn't report a text encoding. (#216)
- CLI: exceptions without a message now still exit with a non-blank line on stderr. (#216)
Full changelog: v0.10.1...v0.11.0