Skip to main content

pyDeprecate

Simple tooling for marking deprecated functions or classes and re-routing to their successors.

PyPI - Python Version License FOSSA Status

PyPI Status PyPI Status Conda Conda CodeFactor

CI testing Code formatting codecov pre-commit.ci status


📋 Table of Contents

📖 Overview

The common use-case is moving your functions across a codebase or outsourcing some functionalities to new packages. For most of these cases, you want to maintain some compatibility, so you cannot simply remove the past function. You also want to warn users for some time that the functionality they have been using has moved and is now deprecated in favor of another function (which should be used instead) and will soon be removed completely.

Another good aspect is not overwhelming users with too many warnings, so per function/class, this warning is raised only N times in the preferred stream (warning, logger, etc.).

✨ Features

  • ⚠️ Deprecation warnings are shown once per function by default (prevents log spam)
  • 🔄 Arguments are automatically mapped to the target function
  • 🚫 The deprecated function body is never executed when using target
  • ⚡ Minimal runtime overhead with zero dependencies (Python standard library only)
  • 🛠️ Supports deprecating functions, methods, and classes
  • 📝 Optionally, docstrings can be updated automatically to reflect deprecation
  • 🔍 Preserves original function signature, annotations and metadata for introspection
  • ⚙️ Configurable warning message template and output stream (logging, warnings, custom callable)
  • 🎯 Fine‑grained control: per‑argument deprecation/mapping and conditional skip_if behavior
  • 🧪 Includes testing helpers (e.g., no_warning_call) for deterministic tests
  • 🔗 Compatible with methods, class constructors and cross‑module moves

💾 Installation

Simple installation from PyPI:

pip install pyDeprecate
Other installations

Simply install with pip from source:

pip install https://github.com/Borda/pyDeprecate/archive/main.zip

🚀 Quick Start

Here's the simplest way to get started with deprecating a function:

from deprecate import deprecated


# Your new function
def new_sum(a: int = 0, b: int = 3) -> int:
    return a + b


# Mark the old one as deprecated and forward calls automatically
@deprecated(target=new_sum, deprecated_in="1.0", remove_in="2.0")
def old_sum(a: int, b: int = 5) -> int:
    pass  # Implementation not needed - calls are forwarded to new_sum


# Using the old function works but shows a warning
result = old_sum(1, 2)  # Returns 3
# Warning: The `old_sum` was deprecated since v1.0 in favor of `__main__.new_sum`.
#          It will be removed in v2.0.

That's it! All calls to old_sum() are automatically forwarded to new_sum() with a deprecation warning.

📚 Use-cases and Applications

The functionality is kept simple and all defaults should be reasonable, but you can still do extra customization such as:

  • 💬 define user warning message and preferred stream
  • 🔀 extended argument mapping to target function/method
  • 🎯 define deprecation logic for self arguments
  • 📊 specify warning count per:
    • called function (for func deprecation)
    • used arguments (for argument deprecation)
  • ⚙️ define conditional skip (e.g. depending on some package version)

In particular the target values (cases):

  • None - raise only warning message (ignore all argument mapping)
  • True - deprecate some argument of itself (argument mapping should be specified)
  • Callable - forward call to new methods (optionally also argument mapping or extras)

➡️ Simple function forwarding

It is very straightforward: you forward your function call to a new function and all arguments are mapped:

def base_sum(a: int = 0, b: int = 3) -> int:
    """My new function anywhere in the codebase or even other package."""
    return a + b


# ---------------------------

from deprecate import deprecated


@deprecated(target=base_sum, deprecated_in="0.1", remove_in="0.5")
def depr_sum(a: int, b: int = 5) -> int:
    """
    My deprecated function which now has an empty body
     as all calls are routed to the new function.
    """
    pass  # or you can just place docstring as one above


# calling this function will raise a deprecation warning:
#   The `depr_sum` was deprecated since v0.1 in favor of `__main__.base_sum`.
#   It will be removed in v0.5.
print(depr_sum(1, 2))
sample output:
3

🔀 Advanced target argument mapping

Another more complex example is using argument mapping is:

Advanced example
import logging
from sklearn.metrics import accuracy_score
from deprecate import deprecated, void


@deprecated(
    # use standard sklearn accuracy implementation
    target=accuracy_score,
    # custom warning stream
    stream=logging.warning,
    # number of warnings per lifetime (with -1 for always)
    num_warns=5,
    # custom message template
    template_mgs="`%(source_name)s` was deprecated, use `%(target_path)s`",
    # as target args are different, define mapping from source to target func
    args_mapping={"preds": "y_pred", "target": "y_true", "blabla": None},
)
def depr_accuracy(preds: list, target: list, blabla: float) -> float:
    """My deprecated function which is mapping to sklearn accuracy."""
    # to stop complain your IDE about unused argument you can use void/empty function
    return void(preds, target, blabla)


# calling this function will raise a deprecation warning:
#   WARNING:root:`depr_accuracy` was deprecated, use `sklearn.metrics.accuracy_score`
print(depr_accuracy([1, 0, 1, 2], [0, 1, 1, 2], 1.23))

sample output:

0.5

⚠️ Deprecation warning only

Base use-case with no forwarding and just raising a warning:

from deprecate import deprecated


@deprecated(target=None, deprecated_in="0.1", remove_in="0.5")
def my_sum(a: int, b: int = 5) -> int:
    """My deprecated function which still has to have implementation."""
    return a + b


# calling this function will raise a deprecation warning:
#   The `my_sum` was deprecated since v0.1. It will be removed in v0.5.
print(my_sum(1, 2))
sample output:
3

🔄 Self argument mapping

We also support deprecation and argument mapping for the function itself:

from deprecate import deprecated


@deprecated(
    # define as deprecation some self argument - mapping
    target=True,
    args_mapping={"coef": "new_coef"},
    # common version info
    deprecated_in="0.2",
    remove_in="0.4",
)
def any_pow(base: float, coef: float = 0, new_coef: float = 0) -> float:
    """My function with deprecated argument `coef` mapped to `new_coef`."""
    return base**new_coef


# calling this function will raise a deprecation warning:
#   The `any_pow` uses deprecated arguments: `coef` -> `new_coef`.
#   They were deprecated since v0.2 and will be removed in v0.4.
print(any_pow(2, 3))
sample output:
8

🔗 Multiple deprecation levels

Eventually you can set multiple deprecation levels via chaining deprecation arguments as each could be deprecated in another version:

Multiple deprecation levels
from deprecate import deprecated


@deprecated(
    True,
    deprecated_in="0.3",
    remove_in="0.6",
    args_mapping=dict(c1="nc1"),
    template_mgs="Depr: v%(deprecated_in)s rm v%(remove_in)s for args: %(argument_map)s.",
)
@deprecated(
    True,
    deprecated_in="0.4",
    remove_in="0.7",
    args_mapping=dict(nc1="nc2"),
    template_mgs="Depr: v%(deprecated_in)s rm v%(remove_in)s for args: %(argument_map)s.",
)
def any_pow(base, c1: float = 0, nc1: float = 0, nc2: float = 2) -> float:
    return base**nc2


# calling this function will raise deprecation warnings:
#   FutureWarning('Depr: v0.3 rm v0.6 for args: `c1` -> `nc1`.')
#   FutureWarning('Depr: v0.4 rm v0.7 for args: `nc1` -> `nc2`.')
print(any_pow(2, 3))

sample output:

8

⚙️ Conditional skip

Conditional skip of which can be used for mapping between different target functions depending on additional input such as package version

from deprecate import deprecated

FAKE_VERSION = 1


def version_greater_1():
    return FAKE_VERSION > 1


@deprecated(True, "0.3", "0.6", args_mapping=dict(c1="nc1"), skip_if=version_greater_1)
def skip_pow(base, c1: float = 1, nc1: float = 1) -> float:
    return base ** (c1 - nc1)


# calling this function will raise a deprecation warning
print(skip_pow(2, 3))

# change the fake versions
FAKE_VERSION = 2

# will not raise any warning
print(skip_pow(2, 3))
sample output:
0.25
4

This can be beneficial with multiple deprecation levels shown above...

🏗️ Class deprecation

This case can be quite complex as you may deprecate just some methods, here we show full class deprecation:

class NewCls:
    """My new class anywhere in the codebase or other package."""

    def __init__(self, c: float, d: str = "abc"):
        self.my_c = c
        self.my_d = d


# ---------------------------

from deprecate import deprecated, void


class PastCls(NewCls):
    """
    The deprecated class shall be inherited from the successor class
     to hold all methods.
    """

    @deprecated(target=NewCls, deprecated_in="0.2", remove_in="0.4")
    def __init__(self, c: int, d: str = "efg"):
        """
        You place the decorator around __init__ as you want
         to warn user just at the time of creating object.
        """
        void(c, d)


# calling this function will raise a deprecation warning:
#   The `PastCls` was deprecated since v0.2 in favor of `__main__.NewCls`.
#   It will be removed in v0.4.
inst = PastCls(7)
print(inst.my_c)  # returns: 7
print(inst.my_d)  # returns: "efg"
sample output:
7
efg

📝 Automatic docstring updates

You can automatically append deprecation information to your function's docstring:

def new_function(x: int) -> int:
    """New implementation of the function."""
    return x * 2


# ---------------------------

from deprecate import deprecated


@deprecated(
    target=new_function,
    deprecated_in="1.0",
    remove_in="2.0",
    update_docstring=True,  # Enable automatic docstring updates
)
def old_function(x: int) -> int:
    """Old implementation that will be removed.

    Args:
        x: Input value

    Returns:
        Result of computation
    """
    pass


# The docstring now includes deprecation information
print(old_function.__doc__)
# Output includes:
# .. deprecated:: 1.0
#    Will be removed in 2.0.
#    Use `__main__.new_function` instead.

This is particularly useful for generating API documentation with tools like Sphinx, where the deprecation notice will appear in the generated docs.

Documentation Sample

🔇 Understanding the void() Helper

When using @deprecated with a target function, the deprecated function's body is never executed—all calls are automatically forwarded. However, your IDE might complain about "unused parameters". The void() helper function silences these warnings:

def new_add(a: int, b: int) -> int:
    return a + b


# ---------------------------

from deprecate import deprecated, void


@deprecated(target=new_add, deprecated_in="1.0", remove_in="2.0")
def old_add(a: int, b: int) -> int:
    return void(a, b)  # Tells IDE: "Yes, I know these parameters aren't used"
    # This line is never reached - call is forwarded to new_add


# Alternative: You can also use pass or just a docstring
@deprecated(target=new_add, deprecated_in="1.0", remove_in="2.0")
def old_add_v2(a: int, b: int) -> int:
    """Just a docstring works too."""
    pass  # This also works

💡 Note: void() is purely for IDE convenience and has no runtime effect. It simply returns None after accepting any arguments.

🧪 Testing Deprecated Code

pyDeprecate provides utilities to help you test deprecated code properly:

from deprecate import deprecated
from deprecate.utils import no_warning_call, void
import pytest


def new_func(x: int) -> int:
    return x * 2


@deprecated(target=new_func, deprecated_in="1.0", remove_in="2.0")
def old_func(x: int) -> int:
    pass


@deprecated(target=new_func, deprecated_in="1.0", remove_in="2.0")
def old_func2(x: int) -> int:
    return void(x)


def test_deprecated_function_shows_warning():
    """Verify the deprecation warning is shown."""
    with pytest.warns(FutureWarning, match="old_func.*deprecated"):
        result = old_func(42)
    assert result == 84


def test_new_function_no_warning():
    """Verify new function doesn't trigger warnings."""
    with no_warning_call(FutureWarning):
        result = new_func(42)
    assert result == 84


def test_no_warning_after_first_call():
    """By default, warnings are shown only once."""
    # First call shows warning
    with pytest.warns(FutureWarning):
        old_func2(1)

    # Subsequent calls don't show warning
    with no_warning_call(FutureWarning):
        old_func2(2)


# call the tests for CI demonstration/validation
test_deprecated_function_shows_warning()
test_new_function_no_warning()
test_no_warning_after_first_call()

The no_warning_call() context manager will fail your test if any warnings of the specified type are raised, ensuring your code is clean.

⚙️ Advanced: Control warning frequency

# Minimal replacement implementation used in examples
def new_func(x: int) -> int:
    return x * 2


# ---------------------------

from deprecate import deprecated


# Show warning every time (useful for critical deprecations)
@deprecated(target=new_func, deprecated_in="1.0", remove_in="2.0", num_warns=-1)
def old_func_always_warn(x: int) -> int:
    pass


# Show warning N times
@deprecated(target=new_func, deprecated_in="1.0", remove_in="2.0", num_warns=5)
def old_func_warn_5_times(x: int) -> int:
    pass

🔧 Troubleshooting

❗ TypeError: Failed mapping

Problem: TypeError: Failed mapping of 'my_func', arguments missing in target source: ['old_arg']

Cause: Your deprecated function has arguments that the target function doesn't accept.

Solutions
  1. Skip the argument (if it's no longer needed):

    # define a target that ignores the extra arg
    def new_func(required_arg: int, **kwargs) -> int:
        return required_arg * 2
    
    
    # ---------------------------
    
    from deprecate import deprecated
    
    
    # None means skip this argument
    @deprecated(target=new_func, args_mapping={"old_arg": None})
    def old_func(old_arg: int, new_arg: int) -> int:
        pass
    
  2. Rename the argument (if target uses different name):

    def new_func(new_name: int) -> int:
        return new_name * 2
    
    
    # ---------------------------
    
    from deprecate import deprecated
    
    
    # Map old to new
    @deprecated(target=new_func, args_mapping={"old_name": "new_name"})
    def old_func(old_name: int) -> int:
        pass
    
  3. Use target=True for self-deprecation (deprecate argument of same function):

    from deprecate import deprecated
    
    
    # Deprecate within same function
    @deprecated(target=True, args_mapping={"old_arg": "new_arg"})
    def my_func(old_arg: int = 0, new_arg: int = 0) -> int:
        return new_arg * 2
    

❗ TypeError: User function 'should_ship' shall return bool

Problem: TypeError: User function 'should_ship' shall return bool, but got: <type>

Cause: When using skip_if with a callable, the function must return a boolean value.

Solution
# Minimal replacement function for examples
def new_func() -> str:
    return "Hi!"


# ---------------------------

from deprecate import deprecated


# Correct: function returns bool
def should_skip() -> bool:
    return False  # replace with your condition


@deprecated(target=new_func, skip_if=should_skip)
def old_func1():
    pass


# Also correct: use a lambda
@deprecated(target=new_func, skip_if=lambda: False)
def old_func2():
    pass

⚠️ Warning Not Showing

Problem: You don't see the deprecation warning.

Cause: By default, warnings are shown only once per function to prevent log spam.

Solutions
# Minimal replacement function for examples
def new_func(x: int) -> int:
    return x * 2


# ---------------------------

from deprecate import deprecated


# Show warning every time
@deprecated(target=new_func, num_warns=-1)  # -1 means unlimited
def old_func_always_warn():
    pass


# Show warning N times
@deprecated(target=new_func, num_warns=5)  # Show 5 times
def old_func_warn_n_times():
    pass

📦 Deprecation Not Working Across Modules

If you're moving functions to a different module or package, show the pattern rather than importing a non-existent package in the docs.

The warning will correctly show the full path for real imports when used in your package.

🤝 Contributing

Have you faced this issue in the past or are you facing it now? Do you have good ideas for improvement? All contributions are welcome!

Please read our Contributing Guide for details on how to contribute, and our Code of Conduct for community guidelines.

Metadata

Release files for pyDeprecate 0.4.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for pyDeprecate 0.4.0
File Size Uploaded
pydeprecate-0.4.0.tar.gz 27.7 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for pyDeprecate 0.4.0
File Interpreter ABI Platform
pydeprecate-0.4.0-py3-none-any.whl Python 3 none any Details

Total release size: 49.5 kB

Release files / pydeprecate-0.4.0.tar.gz

Download URL pydeprecate-0.4.0.tar.gz
Size 27.7 kB
Tags Source
SHA-256 checksum
How to use checksums
7c07295ee6faf427686a458ddc3406014124ac0fb6ad8f90e50eb9d21adb86f3
BLAKE2b-256 checksum
How to use checksums
2ffa1d6a26185653e3b65f2ddcee7f716a9d41f79a39ffb0e9dd9bf0ce43e587
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.9.25

Release files / pydeprecate-0.4.0-py3-none-any.whl

Download URL pydeprecate-0.4.0-py3-none-any.whl
Size 21.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
5f3a07fddb03829bb934163076853ce6ead460b2ec2ba7e0fdaad3e4fa93c186
BLAKE2b-256 checksum
How to use checksums
20e4d618f8eec6bfbab64e2af8a5359a423470a2cad60ef8ea60e8a60be28517
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.9.25
Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page