We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Capstone Projects step 2 of 6
Capstone: Give Each Upload State Its Own Type
An upload starts as a draft. You add chunks of bytes to it, then seal it to get a checksum. Build this API so that mypy rejects a call made on the wrong state, such as asking a draft for its checksum, before the code runs.
import hashlib
empty = begin("report")
first = add_chunk(empty, b"he")
complete = add_chunk(first, b"llo")
sealed = seal(complete)
assert checksum_of(sealed) == hashlib.sha256(b"hello").hexdigest()
assert checksum_of(seal(first)) == hashlib.sha256(b"he").hexdigest()
assert describe(empty) == "draft(report)"
assert describe(sealed) == "sealed(report)"
Give each state its own type
Typestate means giving each state of an object its own class. Each function then accepts only the state it can handle, and mypy rejects a call on the wrong one. Here is the idea with a door:
from dataclasses import dataclass
@dataclass(frozen=True)
class OpenDoor:
room: str
@dataclass(frozen=True)
class ClosedDoor:
room: str
def walk_through(door: OpenDoor) -> str:
return f"You are in the {door.room}."
walk_through(ClosedDoor("kitchen"))
error: Argument 1 to "walk_through" has incompatible type "ClosedDoor"; expected "OpenDoor" [arg-type]
Write these types and functions, and keep the starter's __all__:
type Upload = Draft | Sealed
def begin(name: str) -> Draft: ...
def add_chunk(draft: Draft, data: bytes) -> Draft: ...
def seal(draft: Draft) -> Sealed: ...
def checksum_of(sealed: Sealed) -> str: ...
def describe(upload: Upload) -> str: ...
- Make
DraftandSealedtwo different classes.Uploadis the type for code that accepts either one. - Keep each parameter type as shown.
add_chunkandsealtake aDraft, andchecksum_oftakes aSealed. - Let the signatures do the checking. Don't add
isinstancechecks,assertorraise TypeErrortoadd_chunkorchecksum_of.
Never change a draft
begin(name)returns an empty draft. Keep the name exactly as given, spaces and punctuation included.add_chunk(draft, data)returns a new draft that holds the earlier bytes followed bydata. The draft you passed in stays as it was.- Assigning to a draft's
nameraises an exception. A frozen dataclass does this for you. Store the chunks in a tuple, becausefrozen=Truedoes not stop someone appending to a list field. - Sealing a draft does not use it up. You can seal the same draft again, or add more chunks to it, and each call returns a separate value.
Seal and describe
seal(draft)returns aSealedwhose checksum is the SHA-256 hex digest of all the chunk bytes joined in order. The name is not part of the digest.- An empty draft is valid, and its checksum is the digest of
b"". An empty chunk leaves the checksum unchanged. checksum_of(sealed)returns that checksum.describe(upload)returns exactly"draft(<name>)"or"sealed(<name>)". Handle the two classes in amatchthat ends withcase _: assert_never(upload). If someone later adds a third class toUpload, mypy then reports the case you have not handled.
Keep the starter's type checks
The starter's _negative_fixtures function never runs, but mypy checks it.
- Its four
assert_typecalls check what each function returns.assert_typedoes nothing at runtime. mypy reports an error when the type differs. - Its three calls marked
# type: ignore[arg-type]are wrong-state calls, such aschecksum_of(begin("a")). In strict mode, mypy reports any ignore comment that hides no error. So ifchecksum_ofaccepted anUpload, its wrong-state call would stop being an error, and mypy would fail the file with[unused-ignore]. - Keep the fixtures as they are, and pass
mypy --strictwithout adding atype: ignoreof your own.
What the driver checks
Keep solve as it is. It starts a draft named name, adds each string in
chunks encoded as UTF-8, and keeps every draft along the way.
checksumis the checksum of the final draft.prefixeslists the checksum of every draft, from the empty one to the final one. The driver seals the newest draft first, so anadd_chunkthat changes its input shows up as wrong checksums for the earlier drafts.immutableisTruewhen assigning to a draft'snameraises.distinctisTruewhenDraftandSealedare different classes.describeholdsdescribe()of the final draft and of its sealed upload.
Going further
Python does not check type hints when the code runs. A caller that skips
mypy can still pass a draft to checksum_of. In a library, add runtime
checks at the public functions too. The typing guide covers
exhaustiveness checking
and assert_type.
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.