We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Failure by Design step 13 of 18
Deprecating public API: @warnings.deprecated
Removing a public symbol without a deprecation cycle breaks other people’s
builds with no migration path. Adding a note to the docstring instead means
nobody finds out until you delete it. The middle path is a machine-readable
deprecation, and since Python 3.13 the standard spelling is
@warnings.deprecated (PEP 702).
from warnings import deprecated
@deprecated("use Client.fetch() instead; removal in 4.0")
def fetch_sync(url: str) -> bytes: ...
What that actually does:
-
On a function, emits a
DeprecationWarningwhen it is called. - On a class, emits one on instantiation and on subclass creation — both, because both are ways of depending on it.
-
Sets
__deprecated__to the message, so tooling can read it without parsing your source. -
category=Nonemakes it static-only: type checkers report it, nothing happens at runtime. That is the right setting for a hot path you do not want to slow down. -
Place it after
@overload, not before, when you are deprecating one overload. -
Available from
typing_extensionsfor 3.12 and earlier.
And the part everybody trips over: DeprecationWarning is hidden by default
outside __main__. Your users will not see it unless they ask. Run
python -W error::DeprecationWarning in CI — yours and, if you can persuade
them, theirs.
Deprecation is a process, not a value
A pure function cannot be a deprecation cycle. What it can be is the check a
type checker runs: given a module’s symbol table and a list of places those
symbols are used, produce the diagnostics. Building it teaches you the shape of
the check — and the shape is why __deprecated__ exists as an attribute rather
than a docstring convention.
def deprecation_report(
module_symbols: Mapping[str, str | None],
usages: Sequence[tuple[str, str]],
) -> list[str]:
-
module_symbolsmaps every public symbol name to its deprecation message, or toNonewhen the symbol is fine. Mirroring__deprecated__: present means deprecated, absent means not. -
usagesis a sequence of(caller, symbol)tuples — one per site where a symbol is referenced.
Rules:
-
A symbol in
usagesthat is not a key ofmodule_symbolsis not a deprecation finding, it is a bug in the caller list. RaiseKeyError(symbol)as soon as you meet it. -
Skip symbols whose entry is
None. -
Emit one diagnostic per distinct
(caller, symbol)pair, however many times that pair appears. Ten calls to the same deprecated helper from the same module is one problem to fix, not ten. -
Sort the output by
(caller, symbol). Unstable diagnostic ordering makes a CI diff unreadable.
The format mirrors mypy’s own deprecated error code exactly, including the
two spaces before the bracket:
{caller}: error: {symbol} is deprecated: {message} [deprecated]
The probe
def solve(
module_symbols: Mapping[str, str | None],
usages: Sequence[tuple[str, str]],
) -> dict[str, object]:
Call deprecation_report and convert the KeyError into a reportable value:
{"outcome": "ok", "symbol": None, "diagnostics": [...]}
{"outcome": "unknown-symbol", "symbol": <the offending name>, "diagnostics": []}
Typing notes
Mapping[str, str | None] is the whole design in one annotation: None is a
value meaning “not deprecated”, so if module_symbols[symbol] is not None is
the test, and if module_symbols[symbol] is a bug waiting for the day someone
deprecates something with an empty message.
usages is a sequence of tuples, not of lists — tuple[str, str] is a
fixed-arity heterogeneous record and mypy holds you to the arity. Unpacking it
in the loop header (for caller, symbol in usages) is what makes both names
str without a single annotation.
Stuck?
Python reference solution
Sign in to attempt this problem and reveal the reference solution.