Skip to content

Deprecator Cookbook

This cookbook provides practical recipes for common deprecation scenarios.

Table of Contents

Basic Deprecation Patterns

Setting Up Deprecator

First, initialize deprecator in your project:

deprecator init

This creates _deprecations.py with your deprecator instance:

from deprecator import for_package

deprecator = for_package(__package__)

Defining Deprecations

Define deprecations as module-level constants using UPPER_CASE naming:

# _deprecations.py
OLD_API_DEPRECATION = deprecator.define(
    "old_api() is deprecated, use new_api() instead",
    warn_in="2.0.0",
    gone_in="3.0.0"
)

LEGACY_FORMAT_DEPRECATION = deprecator.define(
    "Legacy format support will be removed",
    warn_in="1.5.0",
    gone_in="2.0.0",
    replace_with="Use JSON format instead"
)

Function and Method Deprecations

Simple Function Deprecation

from ._deprecations import OLD_API_DEPRECATION

@OLD_API_DEPRECATION.apply
def old_api(data):
    """Process data using the old API."""
    # Existing implementation
    return process_data(data)

# The new API that replaces it
def new_api(data, *, format="json"):
    """Process data using the new API with format support."""
    return process_data(data, format=format)

Method Deprecation in Classes

from ._deprecations import LEGACY_METHOD_DEPRECATION

class DataProcessor:
    @LEGACY_METHOD_DEPRECATION.apply
    def process_legacy(self, data):
        """Legacy processing method."""
        return self.process(data, legacy=True)

    def process(self, data, *, legacy=False):
        """Modern processing method."""
        # Implementation
        pass

Deprecating Optional Parameters

When you need to deprecate a parameter but maintain backward compatibility:

from ._deprecations import OLD_PARAM_DEPRECATION

def process_data(data, old_format=None, *, new_format=None):
    """Process data with format specification."""
    if old_format is not None:
        OLD_PARAM_DEPRECATION.warn()
        new_format = old_format

    # Process with new_format
    return do_processing(data, new_format)

Class Deprecations

Entire Class Deprecation

from ._deprecations import OLD_CLASS_DEPRECATION

@OLD_CLASS_DEPRECATION.apply
class LegacyProcessor:
    """This entire class is deprecated."""
    def process(self, data):
        return data

# Modern replacement
class DataProcessor:
    """Modern processor that replaces LegacyProcessor."""
    def process(self, data):
        return enhanced_processing(data)

Deprecating Class Inheritance

from ._deprecations import BASE_CLASS_DEPRECATION

@BASE_CLASS_DEPRECATION.apply
class DeprecatedBase:
    """Base class that should not be used anymore."""
    pass

# Encourage composition or different base
class ModernBase:
    """Use this base class instead."""
    pass

Parameter Deprecations

Keyword Argument Renaming

from ._deprecations import PARAM_NAME_DEPRECATION

def configure(*, new_name=None, old_name=None):
    """Configure with renamed parameter."""
    if old_name is not None:
        PARAM_NAME_DEPRECATION.warn()
        if new_name is None:
            new_name = old_name

    if new_name is None:
        raise ValueError("new_name is required")

    # Use new_name
    do_configuration(new_name)

Deprecating Positional Arguments

from ._deprecations import POSITIONAL_ARG_DEPRECATION

def api_call(endpoint, *, data=None, **kwargs):
    """
    API call that no longer accepts positional data argument.

    Old usage: api_call('/users', {'name': 'Alice'})
    New usage: api_call('/users', data={'name': 'Alice'})
    """
    # Check if data was passed positionally (via kwargs trick)
    if 'deprecated_data' in kwargs:
        POSITIONAL_ARG_DEPRECATION.warn()
        data = kwargs.pop('deprecated_data')

    return make_request(endpoint, data)

Module-Level Deprecations

Deprecating Module Imports

Create a _deprecated_module.py:

# _deprecated_module.py
from ._deprecations import MODULE_DEPRECATION

# Emit warning on import
MODULE_DEPRECATION.warn()

# Re-export from new location for compatibility
from new_module import *

__all__ = ['exported_function', 'ExportedClass']

Deprecating Module Attributes

# module.py
from ._deprecations import ATTRIBUTE_DEPRECATION

def __getattr__(name):
    if name == "OLD_CONSTANT":
        ATTRIBUTE_DEPRECATION.warn()
        return "old_value"
    raise AttributeError(f"module {__name__!r} has no attribute {name!r}")

Testing Deprecations

Testing That Deprecation Warnings Are Emitted

import pytest
from mypackage._deprecations import OLD_API_DEPRECATION

def test_old_api_emits_warning():
    """Test that old_api emits deprecation warning."""
    with pytest.warns(type(OLD_API_DEPRECATION)):
        from mypackage import old_api
        result = old_api("data")

    assert result == expected_result

Testing Version Transitions

from packaging.version import Version
from mypackage import deprecator

def test_deprecation_categories():
    """Test that deprecations have correct categories based on version."""
    # Simulate different versions
    future_deprecator = deprecator.for_package("mypackage", Version("3.0.0"))

    warning = future_deprecator.define(
        "Test warning",
        warn_in="2.0.0",
        gone_in="3.0.0"
    )

    # Should be expired at version 3.0.0
    assert "ExpiredDeprecationWarning" in str(type(warning))

Using pytest Fixtures for Deprecation Testing

# conftest.py
import pytest
from packaging.version import Version
from deprecator import for_package

@pytest.fixture
def test_deprecator():
    """Deprecator for testing with controllable version."""
    return for_package("test-package", Version("1.0.0"))

# test_deprecations.py
def test_warning_emission(test_deprecator):
    warning = test_deprecator.define(
        "Test deprecation",
        warn_in="0.5.0",
        gone_in="2.0.0"
    )

    with pytest.warns(DeprecationWarning):
        warning.warn()

CI/CD Integration

GitHub Actions Integration

Add to your test workflow:

- name: Run tests with deprecation checking
  run: |
    pytest --deprecator-error --deprecator-github-annotations

This will: - Fail tests if expired deprecations are found (--deprecator-error) - Output GitHub annotations for deprecation warnings (--deprecator-github-annotations)

Pre-commit Hook

Create .pre-commit-config.yaml:

repos:
  - repo: local
    hooks:
      - id: validate-deprecations
        name: Validate deprecations
        entry: deprecator validate-package
        language: system
        args: [your-package-name]
        pass_filenames: false

Monitoring Deprecation Status

Add a CI job to monitor deprecations:

#!/bin/bash
# check-deprecations.sh

# Show all deprecations from the default registry
deprecator show-registry

# Validate package configuration
deprecator validate-package mypackage

# Exit with error if expired deprecations exist
if deprecator show-registry | grep -q "expired"; then
    echo "ERROR: Expired deprecations found!"
    exit 1
fi

Best Practices

  1. Define deprecations early: Add deprecations at least one major version before removal
  2. Use clear messages: Explain what's deprecated and what to use instead
  3. Be consistent with versions: Follow semantic versioning for warn_in and gone_in
  4. Test your deprecations: Ensure warnings are emitted and upgrade paths work
  5. Document migrations: Provide clear migration guides for deprecated features
  6. Group related deprecations: Define related deprecations together in _deprecations.py
  7. Monitor in CI: Use pytest plugin and CLI tools to catch expired deprecations early

Migration Example

Here's a complete example of migrating from an old API to a new one:

# _deprecations.py
from deprecator import for_package

deprecator = for_package(__package__)

# Define the deprecation timeline
OLD_PROCESS_API = deprecator.define(
    "process() is deprecated. Use process_data() with format parameter",
    warn_in="2.0.0",
    gone_in="3.0.0"
)

# api.py
from ._deprecations import OLD_PROCESS_API

@OLD_PROCESS_API.apply
def process(data, type="json"):
    """Old API - will be removed in 3.0.0."""
    return process_data(data, format=type)

def process_data(data, *, format="json"):
    """New API with keyword-only format parameter."""
    # Modern implementation
    return do_processing(data, format)

# Allow both during transition period
__all__ = ["process", "process_data"]  # Remove "process" in 3.0.0

Users can gradually migrate:

# Old code (generates warning in 2.0+, fails in 3.0+)
result = process(my_data, "xml")

# New code (works in all versions)
result = process_data(my_data, format="xml")