White Paper

This white paper describes the state of the art of deprecation in Python (2026): the building blocks provided by the language, the deprecation marker of the type system (PEP 702), the policies of the Python Standard Library and of major Open Source projects, and the resulting best practices. It ends with the place of the Deprecated Library in this landscape.

What is a good deprecation?

A deprecation announces that a feature (a function, a class, a method, a parameter, a module attribute…) will be removed or changed in a backward incompatible way. It gives the users of a library the time to migrate their code.

A good deprecation answers four questions:

  • What is deprecated?

  • Since when (in which version)?

  • What should be used instead?

  • When will it be removed?

And it reaches the users through several channels:

  1. a runtime warning, emitted when the deprecated feature is used;

  2. a static diagnostic, reported by type checkers and IDEs before the code even runs;

  3. the documentation (docstring, API reference, changelog, migration guide);

  4. a schedule, which is eventually honored: the feature is really removed.

The building blocks of the language

Warning categories: who sees what?

Python emits deprecation warnings with warnings.warn(). The warning category determines who sees the warning with the default warning filters (PEP 565, Python 3.7+):

Category

Shown by default?

Audience

DeprecationWarning

Only when triggered by code in __main__, and by test runners like pytest

Developers using the API (libraries, applications under development)

PendingDeprecationWarning

No

Early notice for developers: the feature will be deprecated later

FutureWarning

Yes

End users of applications: the behavior will change

Custom subclasses are common to allow a precise filtering, for instance: RemovedInDjango70Warning (Django), Pandas4Warning (pandas), MatplotlibDeprecationWarning (Matplotlib) or SADeprecationWarning (SQLAlchemy).

The stack level

A deprecation warning must point to the caller of the deprecated feature, not to the library itself: this is the role of the stacklevel argument of warnings.warn(). Getting it right is harder than it seems (decorators, wrappers, inheritance…): pandas even computes it dynamically with an internal find_stack_level() helper.

Since Python 3.12, the skip_file_prefixes argument makes this robust: the frames of the given files are skipped, so the warning always refers to the first frame outside of the library:

import os
import warnings

_LIB_DIR = os.path.dirname(__file__) + os.sep


def old_function():
    warnings.warn(
        "old_function() is deprecated since 2.0 and will be removed in 3.0, "
        "use new_function() instead",
        DeprecationWarning,
        stacklevel=2,
        skip_file_prefixes=(_LIB_DIR,),
    )

Deprecating a module attribute

A module attribute (a constant, an alias, a function moved elsewhere…) can be deprecated with a module-level __getattr__ (PEP 562), which is only called for the missing names:

# mylib/__init__.py
import warnings

from mylib.core import new_function

_DEPRECATED_ALIASES = {"old_function": "new_function"}


def __getattr__(name: str) -> object:
    if new_name := _DEPRECATED_ALIASES.get(name):
        warnings.warn(
            f"{name} is deprecated, use {new_name} instead",
            DeprecationWarning,
            stacklevel=2,
        )
        return globals()[new_name]
    raise AttributeError(f"module {__name__!r} has no attribute {name!r}")

Since __getattr__ is only called for missing names, the deprecated attribute must not be defined in the module namespace: only its access through __getattr__ emits the warning.

Deprecation in the type system: PEP 702

PEP 702 (Python 3.13) adds the warnings.deprecated() decorator, also available for older Python versions in typing_extensions:

from warnings import deprecated  # or: from typing_extensions import deprecated


@deprecated("Use new_api() instead")
def old_api() -> int:
    return 1


@deprecated("Use NewClass instead", category=FutureWarning)
class OldClass:
    pass

This decorator has two effects:

  • At runtime, it emits a warning when the decorated function is called or the class is instantiated (or subclassed). The category (default: DeprecationWarning) and the stacklevel can be customized; with category=None, no runtime warning is emitted. The message is stored in the __deprecated__ attribute, for runtime introspection.

  • Statically, type checkers report every use of the deprecated object:

    $ mypy --enable-error-code deprecated example.py
    example.py:9: error: function example.old_api is deprecated: Use new_api() instead  [deprecated]
    
    • mypy reports deprecations with the deprecated error code, which is disabled by default; --report-deprecated-as-note turns them into notes, and --deprecated-calls-exclude hides them for given functions, classes or packages.

    • Pyright and Pylance support PEP 702 too (reportDeprecated); the IDEs display deprecated objects with a strikethrough.

The decorator can also be applied to one overload of an overloaded function: this is the typing way to deprecate a signature, for instance a parameter or a type of argument:

from typing import overload
from warnings import deprecated


@overload
def area(width: float, height: float) -> float: ...
@overload
@deprecated("Passing a (width, height) tuple is deprecated, pass two arguments instead")
def area(width: tuple[float, float]) -> float: ...
def area(width, height=None):
    if height is None:
        width, height = width
    return width * height

Here, the type checkers only report the calls matching the deprecated overload. Note that the runtime implementation still has to emit the warning itself for the deprecated case, since the overloads don’t exist at runtime.

PEP 702 has limits: only the decorated classes, functions and overloads are known to the type checkers. Module attributes, parameters of non-overloaded functions or behavior changes are still only visible at runtime and in the documentation.

The policy of the Python Standard Library

The backward compatibility policy of CPython is defined by PEP 387:

  • a deprecation lasts at least two years (two releases), and it is preferred to wait five years before removal;

  • the deprecations are listed in the documentation, with the Python version of the removal (see Deprecations): for instance, datetime.datetime.utcnow() is deprecated since Python 3.12, in favor of datetime.datetime.now(datetime.UTC);

  • a soft deprecation can be used for an API which should no longer be used to write new code, but which remains safe to use in existing code: it is documented and tested, but not developed any further, and its removal is not scheduled. For instance, the optparse and getopt modules are soft deprecated in favor of argparse.

Internally, CPython uses a warnings._deprecated(name, remove=(3, 14)) helper: it formats the message with the removal version, and it raises an error once the removal version is reached, so that a forgotten deprecation breaks the build of the next release instead of lasting forever. For instance, the AST node classes ast.Num, ast.Str… are deprecated since Python 3.8, emit a warning with this helper since Python 3.12 (remove=(3, 14)), and were removed in Python 3.14.

Practices of major Open Source projects

Project

Practice

NumPy

NEP 23: deprecation warnings for at least two releases (one year) before removal. The message gives the version of the deprecation and of the removal. VisibleDeprecationWarning is used when end users must see the warning (likely a bug in the user code). NumPy 2.0 removed many deprecated aliases at once.

pandas

Three-stage policy (PDEP-17, pandas 3.0): a DeprecationWarning first, a FutureWarning in the last minor release before the next major one (for broader visibility), then removal in the major release. Dedicated classes like pandas.errors.Pandas4Warning name the version of the removal.

Django

A class per removal version (RemovedInDjango70Warning…), with the aliases RemovedInNextVersionWarning and RemovedAfterNextVersionWarning, so that projects can filter the warnings without updating their configuration at each release. A public deprecation timeline lists the removals of each version.

SQLAlchemy

A migration mode for the 2.0 major release: in SQLAlchemy 1.4, the RemovedIn20Warning warnings are only emitted when the SQLALCHEMY_WARN_20 environment variable is set. An application running its tests without these warnings (turned into errors) is ready for SQLAlchemy 2.0.

Matplotlib

A rich internal API (matplotlib._api): deprecated(since, alternative=..., removal=...) for functions, classes and properties, and dedicated decorators for signature changes: rename_parameter, delete_parameter, make_keyword_only.

scikit-learn

Renamed or removed public objects and parameters are still supported for two releases, with the sklearn.utils.deprecated decorator, which also updates the docstring.

Pydantic

Since Pydantic 2.7, Field(deprecated=...) accepts a message or a warnings.deprecated() instance: a warning is emitted when the field is accessed, and the generated JSON Schema contains "deprecated": true.

Common threads: a documented and scheduled policy, messages which give the versions and the alternative, dedicated warning classes for filtering, and tools to deprecate parameters, not only whole functions.

Best practices

  1. Write a complete message: what is deprecated, since which version, what to use instead, and when it will be removed.

  2. Choose the category for the audience: DeprecationWarning for developers, FutureWarning for end users, PendingDeprecationWarning for an early notice. A dedicated subclass helps the users to filter the warnings of your library.

  3. Point to the caller: check the stacklevel with tests, or use skip_file_prefixes.

  4. Make it visible to the type checkers with warnings.deprecated() (or typing_extensions.deprecated), including on overloads to deprecate a signature.

  5. Document it: deprecated, versionchanged and versionadded directives in the docstrings, an entry in the changelog, and a migration guide for large changes.

  6. Test it: check that the warning is emitted (pytest.deprecated_call() or pytest.warns()), and run the test suite with the deprecation warnings turned into errors (-W error::DeprecationWarning, or the filterwarnings option of pytest), to detect the use of deprecated features of your own dependencies early.

  7. Schedule and honor the removal: keep a deprecation timeline, and remove the deprecated features in the announced version. A test (or a helper like warnings._deprecated) can fail when the removal version is reached.

  8. Prefer a soft deprecation for a stable API which is only discouraged: document it, but don’t warn and don’t schedule its removal.

Where does the Deprecated Library fit?

The Deprecated Library predates PEP 702 and is complementary to warnings.deprecated():

Feature

warnings.deprecated()

Deprecated Library

Runtime warning for functions, methods and classes

Yes

Yes

Detection by type checkers and IDEs

Yes

No

Message built from a reason and a version

No (free message)

Yes

Docstring update (Sphinx directives, Google or NumPy sections)

No

Yes (deprecated.sphinx, deprecated.google, deprecated.numpy)

Local warning filter (action) and extra_stacklevel

No (only stacklevel)

Yes

Deprecation of parameters

Only through overloads (static only)

Yes, at runtime (deprecated_params())

Python versions

3.13+ (older versions: typing_extensions)

3.12+

The type checkers only recognize warnings.deprecated() and typing_extensions.deprecated: the @deprecated decorators of this library are not reported statically. To get the best of both worlds, you can combine them: warnings.deprecated() with category=None for the static detection only, and the Deprecated Library for the runtime warning and the documentation:

import warnings

from deprecated.sphinx import deprecated


@warnings.deprecated("Use new_api() instead", category=None)  # type checkers and IDEs only
@deprecated(reason="Use :func:`new_api` instead", version="2.0")  # runtime warning + docstring
def old_api() -> int:
    """Old API."""
    return 1

With this combination, calling old_api() emits a single runtime warning (from the Deprecated Library, pointing to the caller), the docstring contains the .. deprecated:: 2.0 directive, and the type checkers report every use of old_api.

References