You want to rename clean_text() to normalize(). If you delete
clean_text, every program that calls it breaks on the day it upgrades your
library. Instead, keep clean_text working for a while and tell its callers to
move. That is a deprecation. The old name still works, and it warns that
it will go away.
from warnings import deprecated
def normalize(text: str) -> str:
return text.strip().lower()
@deprecated("use normalize() instead, clean_text() goes away in 4.0")
def clean_text(text: str) -> str:
return normalize(text)
deprecated is in the warnings module from Python 3.13. On Python 3.12,
import the same decorator from the typing_extensions package.
What @deprecated does
The decorator does three things:
- Each call to
clean_textgives aDeprecationWarningwith your message. The decorator sets the stack level for you, so the warning names the caller's line. - It stores the message in
clean_text.__deprecated__. - It marks
clean_textas deprecated for type checkers, which then report every use without running the code.
Warn Callers with the warnings Module, earlier in this track, shows how
filters decide whether Python prints the warning, hides it or raises it. With
an error filter, the call raises before the body of clean_text runs. On a
class, @deprecated warns when someone creates an instance or subclasses it.
mypy reports deprecated names only when you turn that check on, even in
strict mode. Here app.py imports and calls clean_text:
$ mypy --strict app.py
Success: no issues found in 1 source file
$ mypy --strict --enable-error-code deprecated app.py
app.py:1: error: function labels.clean_text is deprecated: use normalize() instead, clean_text() goes away in 4.0 [deprecated]
Found 1 error in 1 file (checked 1 source file)
With @deprecated("...", category=None), a call gives no warning at
runtime. The type checker still reports it. Use that only when you know your
callers run a type checker.
Deprecate a parameter, not the whole function
Sometimes only one argument changes. Here timeout_s becomes timeout. Warn
only when a caller passes the old name, and keep the old name working:
import warnings
def timeout_value(
*, timeout: float | None = None, timeout_s: float | None = None
) -> float | None:
if timeout_s is not None:
if timeout is not None:
raise TypeError("pass timeout or timeout_s, not both")
warnings.warn(
"timeout_s is deprecated, use timeout",
DeprecationWarning,
stacklevel=2,
)
return timeout_s
return timeout
Here you call warnings.warn yourself, so you pass stacklevel=2 to name the
caller's line. Decide what happens when a caller passes both names. This code
raises TypeError.
This code treats None as "not passed". If None is a value callers pass on
purpose, use a sentinel default instead. A sentinel is a private object,
such as _MISSING = object(), that no caller can pass by accident.
Find the calls before you remove the function
Run your tests with -W error::DeprecationWarning. Each deprecated call then
raises, and the test that makes it fails. That finds only the calls your tests
reach. mypy with --enable-error-code deprecated finds the others, because it
reads the code without running it.
Tell people how to move. Put a before-and-after example in your release notes, and say which release removes the old name. If your project uses semantic versioning, add the warning in a minor release, such as 3.4, and remove the old name in the next major release, 4.0. If there is no replacement, say so, and say why.
Check your understanding
Your test suite passes after you add @deprecated to clean_text. Does that mean none of your code still calls it?
Show answer
Hide answer
No. A warning is only a message, so a test passes whether the runner prints
the warning or hides it. Run the tests with -W error::DeprecationWarning, so
each deprecated call fails the test that makes it. That still misses calls the
tests never reach, so also run mypy with --enable-error-code deprecated,
which checks every line.
The next exercise, Report Calls to Deprecated Functions, builds the report a checker gives. You get a table of deprecated names and a list of calls. You write one line for each caller that uses a deprecated name.
Going further
- To deprecate one way of calling an overloaded function, put
@deprecatedunder@overloadon that one signature. Type checkers report calls that match it. Overloads never run, so those calls give no runtime warning. @deprecatedcomes from PEP 702. See thewarnings.deprecateddocumentation.