PCPP1 study materials
PCPP1 study resources

Python 3.8 support, Ruff's UP rules, and a patch release that breaks defaults

PCPP1 · Put your knowledge to work in context.

All case studies

Python 3.8 support, Ruff's UP rules, and a patch release that breaks defaults

acme-utils is a shared library installed by 30 internal services. pyproject.toml declares requires-python = ">=3.8" and CI runs a matrix of 3.8, 3.11 and 3.12. Only half the modules start with from __future__ import annotations. A recent change replaced typing.List with list[...] and Optional[X] with X | None throughout, after ruff was configured with target-version = "py312" and the UP rule set. Tests pass on 3.11/3.12, but on 3.8 a service crashes when it calls typing.get_type_hints() on a public function, and another service gets TypeError: unsupported operand type(s) for | from a runtime isinstance check. Meanwhile release 1.4.3 changed the default timeout argument of fetch() from 5 to 10 seconds and removed a deprecated fetch_all() alias; it was published as a patch. Two downstream teams report broken builds. The maintainers want a policy they can enforce in CI rather than another one-off fix.

1. Diagnose the Python 3.8 failures and give a configuration and CI policy for the library. Be specific about what `from __future__ import annotations` does and does not fix.

Read the worked answer

The failures are runtime evaluation problems, not parser problems. Python 3.8 can parse list[int] and X | None, but evaluating list[int] raises TypeError: 'type' object is not subscriptable, and evaluating int | None raises TypeError: unsupported operand type(s) for |. from __future__ import annotations only postpones evaluation of annotations; it does not make typing.get_type_hints() safe, because that function evaluates the stored strings in the module globals. A runtime isinstance(x, int | None) is ordinary code, so the future import has no effect there at all.

The CI configuration caused this: ruff with target-version = 'py312' and the UP rules is allowed to rewrite typing.List to list and Optional[X] to X | None, which is only safe if every annotation is never evaluated. A defensible policy for a >=3.8 library: set target-version = 'py38' and mypy python_version = '3.8'; add from __future__ import annotations to every module (ruff FA100/FA102 help); set lint.pyupgrade.keep-runtime-typing = true or ignore UP006/UP007 where annotations are runtime-evaluated; and add a 3.8 CI job that imports every public module and calls typing.get_type_hints() on public functions and classes. For isinstance, use (int, type(None)), not a PEP 604 union. If 3.8 support is not worth this cost, drop it in a major release and make the matrix honest.

2. Was 1.4.3 an acceptable SemVer patch? Give the correct release strategy, including a deprecation path for `fetch_all()` and the default timeout change. How would you prevent the next occurrence in CI?

Read the worked answer

Release 1.4.3 contains two incompatible changes: removing fetch_all() and changing fetch()'s default timeout from 5 to 10 seconds. The second is easy to miss because the signature still accepts timeout, but callers that relied on failing fast now block longer; observable default behavior is part of the public contract. Under SemVer, for a 1.x library, either change alone requires 2.0.0. A patch release must be limited to backwards-compatible bug fixes.

The repair path: yank or deprecate 1.4.3, publish 1.4.4 that restores the old default and the alias, and add a DeprecationWarning in fetch_all() that names the replacement and the planned removal version. Keep the alias for at least one minor release and a documented window, then remove it in 2.0.0. If the new timeout default is desired, keep 5 as the default and let callers opt into 10, or ship the change in 2.0.0 with a migration note. Enforce the policy with an API-diff check (griffe or a similar tool) against the previous release, a test run with -W error::DeprecationWarning for internal callers, and a changelog template that asks whether the change alters a default, an exception type, or a public symbol.