Deprecation Policy#

Note

This document is the canonical policy for retiring public API in process-improve. Tracks ENG-22.

Scope#

This policy covers anything documented as public:

  • Top-level imports (process_improve.PCA, process_improve.lm).

  • Public methods and attributes on documented classes (PCA.scores_, PLS.predict, MCUVScaler.fit).

  • The public signatures of those methods (kwarg names, defaults, keyword-only-ness).

  • The MCP tool surface declared via @tool_spec: tool names, schema keys, and the meaning of each field.

Private API (anything prefixed with _, anything under process_improve._internal or process_improve._linalg, and anything not re-exported from a package __init__.py) may change without notice.

The contract#

For any breaking change to a public surface, follow this schedule:

Phase

At least one MINOR release each

What contributors do

Announce

X.Y.0

Add the new API. Emit a DeprecationWarning from the old API. Document the rename in the docstring. Add a CHANGELOG entry.

Warn

X.(Y+1).* and on

Old API still works; the warning still fires. CHANGELOG carries “Deprecated since X.Y; will be removed in (X+1).0”.

Remove

(X+1).0

Old API is deleted. CHANGELOG entry under “Removed”.

In words: one MINOR cycle of “you can keep using this, but here is the new name” plus a second cycle of “we mean it” before removal at the next MAJOR.

Mechanism#

A renamed attribute or function uses the helper warnings.warn from the standard library with the DeprecationWarning category and stacklevel=2 (consistent with Error-Handling Style Guide):

import warnings

def old_name(*args, **kwargs):
    warnings.warn(
        "process_improve.X.old_name is deprecated since 1.23.0 "
        "and will be removed in 2.0; use X.new_name instead.",
        category=DeprecationWarning,
        stacklevel=2,
    )
    return new_name(*args, **kwargs)

Required fields in the message:

  • The fully qualified deprecated name.

  • The version that announced the deprecation (deprecated since X.Y.0).

  • The version that will remove it (will be removed in (X+1).0).

  • The intended replacement (or “no replacement; the functionality is no longer supported”).

For renamed attributes on PCA / PLS / TPLS, the existing __getattr__ migration helper is the right mechanism; it should be updated to emit a DeprecationWarning rather than raising immediately. The helper is a single place to enforce the message format.

Changing a default#

A new default that changes the numbers a call returns, rather than retiring a name, follows the same schedule. Its risk is different: code that keeps running, now on other results, is wrong without being broken. So:

  • The parameter’s default becomes None, which resolves to the old value until (X+1).0 and to the new one from then.

  • The warning is a FutureWarning, not a DeprecationWarning. Python shows a FutureWarning to end users by default; it hides a DeprecationWarning outside __main__ and test runs, so the people whose results will change would never see it.

  • It is issued only by calls whose result the new default would change, and passing the parameter, to either value, silences it. A call the change leaves alone stays quiet.

  • Where the result is a dict that an agent or a tool reads, it also carries the warning’s text (as analyze_experiment’s coding_note did), since a warning does not reach them.

  • Where possible, the new behaviour keeps what the old one gave, under an unmistakable name (coefficients_actual), so that nobody loses a result they relied on.

  • At (X+1).0 the default becomes the new value, and the warning and the note are removed. None stays accepted, as the new value, where a caller or a tool schema can still pass it.

analyze_experiment’s coding was the first example: announced in 1.98.0, its default changed from "actual" to "auto" in 2.0.0.

When a deprecation is unavoidable mid-cycle#

If a bug or security finding forces a behaviour change inside a PATCH release:

  1. The CHANGELOG entry under “Changed” explicitly names the behaviour change.

  2. If the public signature changes, the next MINOR release includes a deprecation shim under this policy. PATCH releases never break a signature.

If the change is forced by a downstream library (a sklearn or statsmodels deprecation cascading through the package), the CHANGELOG cross-references the upstream change.

What is not a breaking change#

  • Adding a new optional kwarg with a default that preserves the previous behaviour.

  • Adding a new public function.

  • Fixing a documented bug (the fix is the breaking change of the bug, not of the API).

  • A purely internal refactor that does not affect any public surface.

Edge case: Bunch return types. The fitted-model predict methods return sklearn.utils.Bunch with named fields. Adding a field is not a breaking change. Renaming or removing one is.

Tracking and rollout#

  • The __getattr__ migration helpers on PCA / PLS already exist; they currently raise rather than warn. Migrating them to emit DeprecationWarning is in scope for the next Wave-7 PR (ENG-22).

  • tests/test_deprecation_policy.py reads every warnings.warn(..., DeprecationWarning) call in src/ and checks its message against the format above, so the message is written as a string literal (an f-string is fine) where the call is made. It also compares the removal version with the version in pyproject.toml: once the package reaches it, the test fails, so we cannot forget to delete the shim. The test runs in CI with the rest of the suite.

Cross-references#