We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← The Type System as a Design Tool step 24 of 24
Modernisation kata: inventory.py, 2019 to 2026
This is week one on a legacy codebase, compressed into one module.
You are given inventory.py as it was last touched in 2019 and asked for the
same behaviour, expressed the way the language works now. Not a rewrite — a
modernisation. Every observable output must be identical; the difference is that
afterwards, a whole class of mistake becomes impossible to make.
What is wrong with the module you are given
Read the starter before you read this list. Then check yourself against it.
-
typing.List/Dict/Optionaleverywhere, and aRecord = Dict[str, Any]alias that is theAnyboundary in disguise. -> builtin generics,X | None, and a real structure. -
A module-scope
TypeVar("T", covariant=True)used in a parameter position. That is not merely old spelling: it is unsound, and it is why a naive PEP 695 conversion cannot be done in one step. A covariant parameter in an input position has to be split into read and write halves before the conversion is even legal. -
Quoted forward references and
# type:comments — pre-3.0 syntax kept alive by inertia. -
Magic string flags.
"active"and"discontinued"as bare constants, compared with==, with"activ"a typo away from a silent empty result set. -> aLiteraltype plus a narrowing parser. -
An unbranded
int/strid. A SKU is astr, a name is astr, and nothing stops you passing one where the other belongs. ->NewType. -
list[T]parameters that should beIterable[T]/Sequence[T]. -
A bare
except Exception: return Nonearound parsing, which turns every malformed record into an indistinguishableNoneand loses the reason. -
json.loadsreturningAny, laundered into a “typed” structure. This passes plain--strictsilently — it is the single most valuable thing to fix and the one no gate catches for you.
The task
def solve(
raw: list[dict[str, object]],
status: str,
minimum: int,
) -> tuple[list[str], int, list[str], str]:
Parse each raw record into an immutable Item, collecting failures rather than
discarding them. Then filter by status and by quantity >= minimum, and
return:
- the matching SKUs, sorted;
- their total quantity;
- the parse error messages, in input order;
-
f"{describe(status)}:{first_sku_or_none}".
The parse rules, and the exact error messages, are contract:
| condition | message |
|---|---|
sku missing, not a str, or empty |
bad sku |
name missing or not a str |
bad name |
quantity a bool, not an int, or negative |
bad quantity |
status not one of the two |
bad status: <repr> |
Checked in that order, so a record with two problems reports the first.
describe maps active to "in catalogue" and discontinued to "retired",
and closes its match with assert_never. The fourth element uses "none"
when nothing matched.
The shape to aim for
Roughly: a Status literal alias and a STATUSES tuple; a Sku NewType; a
frozen slotted Item dataclass; a ParseError subclassing ValueError; a
parse_status(raw: object) -> Status that narrows or raises; a
parse_item(record: Mapping[str, object]) -> Item; a PEP 695
first_or[T](items: Iterable[T], default: T) -> T; and a @final Inventory
whose methods take Iterable/Sequence and return list.
Note parse_item takes a Mapping, not a dict — it only reads.
The traps built in
bool is an int subclass. A record with "quantity": true must be
rejected. isinstance(quantity, int) alone accepts it and silently stores 1.
Falsy is not missing. quantity: 0 is a valid record and must survive.
if not quantity: raise would eat it.
record.get("sku") is object, not str. Every field needs an
isinstance narrowing before use, and that narrowing is the schema. Once you
have written it, Any has nowhere to enter the module — which is the actual
deliverable. Passing --strict was never the goal; having no unchecked value
anywhere in the file is.
A rejected record must not consume anything. Errors are collected in input order and the valid records are unaffected by their neighbours.
Gates
mypy --strict plus --warn-unreachable and --extra-checks. Beyond that,
the module should have no Any, no cast, no # type: ignore, no bare
except, no typing.List, and no Generic.
Loading visualization…
Stuck?
Python reference solution
Sign in to attempt this problem and reveal the reference solution.