We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← The Type System as a Design Tool step 1 of 24
Modernise legacy annotations
Write the codemod that drags a 2019 annotation into 2026.
from typing import List in a file that already runs on 3.12 is rarely a lone
mistake. It is a marker: the author has also missed X | Y, PEP 695 generics
and Self, and almost certainly learned Python typing from a tutorial written
before any of them existed. Reviewers read it that way, so the cheapest thing
you can do to a legacy module is make its annotations look their age.
PEP 585 (Python 3.9) made the builtin containers subscriptable:
list[int], dict[str, int], tuple[str, ...], type[Exception], and the
collections.abc originals — Sequence, Mapping, Callable, Iterable —
instead of their typing shadows. PEP 604 (3.10) made X | Y a real
union type at runtime, which retires both Union and Optional.
The honest nuance, because you will be asked in review: the typing aliases
are deprecated, but removal is not currently planned (cpython#106745).
Nothing will break if you leave them. The names with actual deadlines are
different ones: ByteString (removal in 3.17), AnyStr (3.18), and
no_type_check_decorator (3.15).
And this matters for how you gate it: mypy --strict does not flag
typing.List — not even with --enable-error-code deprecated. Modernisation
is entirely ruff’s job, rules UP006 and UP035. If you thought your type
checker was catching this, it never was.
The task
def modernise(annotations: list[str]) -> tuple[list[str], list[str]]:
You receive a list of annotation expressions as strings. Return a 2-tuple:
- the same annotations rewritten in modern spelling, in the same order;
-
the
importlines the rewritten module now needs, one per module, sorted by module name, with the imported names sorted inside each line.
The rewrite rules, exactly:
| Legacy | Modern | Import needed |
|---|---|---|
List Dict Tuple Set FrozenSet Type |
list dict tuple set frozenset type |
none — they are builtins |
Optional[X] |
X | None |
none |
Union[A, B] |
A | B |
none |
Callable Sequence Iterable Iterator Mapping MutableMapping MutableSequence Hashable Awaitable Coroutine Generator |
unchanged |
from collections.abc import ... |
Any Final ClassVar Literal Never NoReturn Self |
unchanged |
from typing import ... |
anything else (int, Exception, MyModel) |
unchanged | none |
Two module constants and a split_top helper are given to you — split_top
splits on a separator only where bracket depth is zero, which is what makes the
nesting tractable. The rest is a small recursive rewrite.
Details that the tests pin down:
-
Nesting is arbitrary.
Dict[str, List[int]]becomesdict[str, list[int]]; the rewrite has to recurse into arguments. -
Unions flatten and deduplicate.
Union[int, Optional[str]]isint | str | None, notint | str | None | Noneand notint | (str | None).Union[int, int]collapses toint. -
Callable‘s first argument is a bracketed list, not a type.Callable[[int, str], X]must keep the inner[int, str]intact — andCallable[..., None]must keep its literal.... -
Anysurvives. There is no builtin equivalent, so it stays, and it still needs itstypingimport. PreservingAnyis not an endorsement — see theAnyversusobjectproblem for why it is the most dangerous name in the module. -
Two annotations that both need
Sequenceproduce one import line.
Emit from collections.abc import Mapping, Sequence, not two lines and not
import collections.abc. When nothing needs importing, the second element is
an empty list.
Why this shape
You are writing the transformation ruff’s UP006/UP035/UP007 perform, on a
deliberately narrowed grammar. Doing it once by hand is the difference between
“the linter yelled and I accepted the fix” and knowing exactly which fixes are
safe. It is not academic: UP007 on a module that calls get_type_hints() at
runtime under Python 3.9 will break it, because int | str was a syntax-level
change, not a library one. Knowing what the codemod does tells you when not to
run it.
Your submission must pass mypy --strict. Note in particular that
--disallow-any-generics rejects a bare dict or list annotation — you must
parameterise the accumulator you use to collect imports.
Stuck?
Python reference solution
Sign in to attempt this problem and reveal the reference solution.