formerly renames a class that users inherit from, without breaking them.
Install with:
pip install formerlyRename the class, and leave the old name behind as an alias:
from formerly import deprecated_class
class NewName(SomeClass): ...
OldName = deprecated_class("OldName", NewName)Now:
- Subclassing
OldNamewarns, pointing at theclassstatement. - Instantiating
OldNamewarns, but instantiating a subclass does not: the subclass author gets one warning where the problem is, not one per object. issubclass()andisinstance()checks againstOldNameaccept subclasses ofNewName, so code that has already migrated keeps passing the checks of code that has not.OldNameis a real class: it pickles, survivescopy.deepcopy(), passesinspect.isclass(), and can be registered with anabc.ABCMetainterface.
Type checkers reject a base class that comes from a variable, so users
subclassing the alias need OldName: Any = ... on their side, or a
per-statement ignore comment.
Warnings use DeprecationWarning and report the paths of the classes
involved. Both are configurable, along with the messages themselves:
OldName = deprecated_class(
"OldName",
NewName,
category=MyLibraryDeprecationWarning,
new_path="mylibrary.NewName",
subclass_message="{cls} inherits from {old}, which is going away in 3.0.",
)By default only the first subclass warns, since one warning is enough to tell
users to migrate. Pass warn_once=False to warn on every subclass.
See the deprecated_class() docstring for the full signature.
Renaming a base class asks more of a deprecation helper than renaming a function does. Each row below is a behavior that a class users inherit from needs:
| formerly | warnings.deprecated | debtcollector | Deprecated | pyDeprecate | |
|---|---|---|---|---|---|
| Warns where the subclass is defined | ✔ | ✔ | ✘ | ✘ | ✘ |
| Stays quiet when a subclass is instantiated | ✔ | ✔ | ✘ | ✘ | ✘ |
| Warns when the old name is instantiated | ✔ | ✔ | ✔ | ✔ | ✔ |
Accepts migrated classes in issubclass() and isinstance() |
✔ | ✘ | ✘ | ✘ | ✔ |
Is a class: pickle, deepcopy, inspect.isclass(), abc |
✔ | ✔ | ✔ | ✔ | ✘ |
| Reported by type checkers | ✘ | ✔ | ✘ | ✘ | ✘ |
Only the fourth row is unique to formerly: the old name keeps accepting
classes that already inherit from the new one, so a library can rename a base
class without breaking the isinstance() checks that its own code, or its
users' code, runs against the old name.
If nothing checks types against the old name, warnings.deprecated covers the
rest, and adds what formerly cannot: type checkers report the deprecation
before the code runs. It needs Python 3.13, or typing_extensions for older
versions, which behaves identically.
Measured on Python 3.13 against Deprecated 1.3.1, debtcollector 3.1.0
and pyDeprecate 0.11.0. pyDeprecate wraps the class in a proxy object
rather than a class, so subclassing it raises TypeError.