We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Precise Type Hints with Literals and Generics step 24 of 26
Capstone: Modernize a Typed Inventory Module
The starter is an inventory module written in 2019. It has Dict[str, Any]
records, a # type: comment, an Item class with no annotations, and a
parser that catches every error and returns None. Turn it into a typed
module that checks each record and says what is wrong with it.
This capstone uses most of the track. It needs a Literal status, a
NewType SKU, narrowing on values read from a record, a frozen dataclass, a
generic helper, assert_never and @final. The parsing behavior changes on purpose. Match
the rules below, not the starter.
records = [
{"sku": "B", "name": "bolt", "status": "active", "quantity": 2},
{"sku": "A", "name": "", "status": "active", "quantity": 0},
{"sku": "B", "name": "spare", "status": "active", "quantity": 3},
{"sku": "", "name": 7},
]
assert solve(records, "active", 0) == (["A", "B", "B"], 5, ["bad sku"], "in catalogue:A")
assert solve(records, "active", 3) == (["B"], 3, ["bad sku"], "in catalogue:B")
assert solve([], "discontinued", 0) == ([], 0, [], "retired:none")
The types
Status, an alias forLiteral["active", "discontinued"], and aSTATUSEStuple of both values.Sku = NewType("Sku", str).Item, a dataclass withfrozen=Trueandslots=True, and the fieldssku: Sku,name: str,status: Statusandquantity: int, in that order.ParseError, a subclass ofValueError.
Parse each record
parse_item(record: Mapping[str, object]) -> Item checks the fields in this
order and raises ParseError for the first one that fails:
| Check | Message |
|---|---|
sku is missing, not a string, or empty |
bad sku |
name is missing or not a string |
bad name |
quantity is missing, a bool, not an int, or negative |
bad quantity |
status is missing or not one of the two statuses |
bad status: <repr> |
- Use
reprin the status message, so"activ"givesbad status: 'activ'and a missing status givesbad status: None. - Do not trim or change case. A SKU of spaces and an empty name are valid.
- A quantity of
0is valid.TrueandFalseare not. - Ignore extra fields, and do not change the record. Use only
Mappingreads, such asrecord.get(...), so a read-only mapping works too.
parse_status(raw: object) -> Status returns the matching Status value, or
raises ParseError with the status message.
Query the items
Inventoryis a class marked@final.Inventory(items: Iterable[Item])stores the items as a tuple.matching(status: Status, minimum: int) -> list[Item]returns a new list of the items with that status andquantity >= minimum, in their original order and with duplicates.minimumcan be negative.total(items: Sequence[Item]) -> intadds up the quantities of the items you pass it, not the whole inventory. An empty sequence gives0.first_or[T](items: Iterable[T], default: T) -> Treturns the first item, even if it is falsy, anddefaultonly for an empty input. Stop after the first item, so an iterator is not used up. Delete the starter's unusedTypeVar.describe(status: Status) -> strreturns"in catalogue"for"active"and"retired"for"discontinued". End itsmatchwithcase _: assert_never(status).
What solve returns
solve(raw: list[dict[str, object]], status: str, minimum: int) returns
tuple[list[str], int, list[str], str].
- Parse every record in order. Catch
ParseErroraround eachparse_itemcall only, and keep its message. - Parse the
statusargument withparse_status. If it is invalid, let theParseErrorpropagate. - Query the inventory, then return four values:
- the matching SKUs as strings, sorted, with duplicates;
- the total quantity of the matching items;
- the record error messages, in input order;
- the description, a colon and the first sorted SKU, or
nonewhen nothing matches.
Modernize the code
- Use
list,dictandX | None, nottyping.ListorOptional. - Annotate every function. Do not use
Any,cast,# type: ignore, a bareexcept, orGeneric. - Delete the unused
loadhelper and thejsonimport, becausesolvereceives records that are already decoded.
Your code, saved here
Sign in to access your accepted solutions and saved draft history. You can keep reading the lesson without an account.
Sign in to your accountStuck?
Python reference solution
Sign in to track your attempts toward unlocking the reference solution.