Skip to content

← Capstones step 1 of 5

Hard End-to-End

Capstone: the upload that cannot be misused

There is nothing interesting to compute in this problem. sha256 does the only arithmetic. The entire assessment is structural, and that is the point: this is what it looks like when the type system, rather than a runtime check, is what keeps an API honest.

The API

Export exactly these names:

Draft, Sealed, Upload
begin(name: str) -> Draft
add_chunk(draft: Draft, data: bytes) -> Draft
seal(draft: Draft) -> Sealed
checksum_of(sealed: Sealed) -> str
describe(upload: Upload) -> str

Draft and Sealed are your design. The grader never looks inside them; it only ever calls the five functions. What matters is that they are two distinct types.

checksum_of returns the hex SHA-256 of the concatenated chunk bytes — the name is not part of the digest. describe returns "draft(<name>)" or "sealed(<name>)".

What “cannot be misused” means

The obvious design is one class with a sealed: bool flag and a raise RuntimeError("already sealed") in add_chunk. That design works. It is also strictly worse, because the error arrives at runtime, in production, on the path nobody exercised — and the caller had no way to know they were writing a bug.

With two types, the mistake is not an error you raise. It is a program that does not type-check:

checksum_of(begin("a"))          # a Draft has no checksum yet
add_chunk(seal(begin("a")), b"") # you cannot append to a sealed upload
seal(seal(begin("a")))           # you cannot seal twice

The feedback writes itself: “your API lets a caller express checksum_of(begin('a')). That should not be possible to express.”

This is the typestate pattern. The state of the object lives in its type, so every state transition is a function with a different input and output type, and an illegal sequence is a compile-time error.

The negative fixture, and why you cannot cheat it

Those three lines live in _negative_fixtures, each with a # type: ignore comment. They are never executed. They are graded anyway, because --strict includes --warn-unused-ignores.

So consider the lazy solution: Draft = Sealed = dict[str, Any]. It passes every behavioural test in this problem. And it fails the submission, because with one type the three calls stop being errors, the three # type: ignore comments become unused, and mypy reports three errors. A negative assertion that cannot be satisfied by weakening the types is the only kind worth writing.

The assert_type calls above them are the positive half: they pin every return type, so you cannot satisfy the negatives by returning Any from somewhere.

Three more properties the driver checks

add_chunk does not mutate its argument. The driver builds the whole chain of drafts and then seals them newest first. If add_chunk appended in place, every prefix would carry the final digest and the whole prefixes list collapses. A frozen dataclass returning a new instance makes this free.

Instances are immutable. setattr(draft, "name", "hijacked") must raise. frozen=True gives you FrozenInstanceError; slots=True also removes the per-instance __dict__, so a typo’d attribute name is an error rather than a new attribute nobody reads.

describe is total. type Upload = Draft | Sealed, matched exhaustively. mypy proves every branch returns; adding case _: assert_never(upload) makes the proof explicit, so that the day someone adds an Aborted state, the failure is a type error in describe rather than a None returned into a log line.

The rule for the implementation

No isinstance, no assert, and no raise TypeError in add_chunk or checksum_of. If you find yourself reaching for one, the types are not doing their job and you have rebuilt the boolean-flag design with extra steps. describe is the one place a runtime discrimination is legitimate — that is what the union is for.

One aside worth keeping: draft_type is not sealed_type in the driver goes through two object locals on purpose. Written directly as Draft is not Sealed, --strict-equality rejects it as a non-overlapping identity check — which is the type checker telling you, unprompted, that your two states are provably distinct.

Loading visualization…