Skip to content
AyoKoding

Beginner Examples

Examples 1-28 cover the foundations every later example builds on: RFC 9110's method safety/idempotency, the status codes that distinguish one outcome from another (201/204/400/401/403/409/422), URI-path and header versioning, offset and cursor pagination (and why cursor is stable under concurrent inserts), Idempotency-Key record/replay/reject-on-mismatch, the repository and unit-of-work patterns over an in-memory store, ACID transaction atomicity, JWT encode/verify and expiry, RBAC vs ABAC gates, and structured JSON logging with a threaded correlation id. Every example is a complete, self-contained, originally-authored Python 3 script using only the standard library -- no framework, driver, or network connection is required. Run each with python3 example.py; every printed line below was captured from an actual run of the file shown, not hand-traced.


Example 1: REST CRUD Endpoints -- One Resource, Four Verbs

ex-01 · exercises co-01

Each CRUD operation maps to one HTTP verb over the same /tasks resource. This example routes every verb to its own handler and verifies each produces the effect its RFC 9110 semantics promise.

learning/code/ex-01-rest-crud-endpoints/example.py

# pyright: strict
"""Example 1: REST CRUD Endpoints -- one resource, four verbs. (co-01)
 
GET/POST/PUT/DELETE each take their OWN path to the SAME /tasks resource.
This example routes every verb to its own handler over one in-memory store
and verifies each verb produces the effect its RFC 9110 semantics promise.
"""
 
from dataclasses import dataclass, field  # => field: mutable-default-safe factories
 
 
@dataclass  # => co-01: the shape every handler below returns -- status + body
class Response:
    status: int  # => the HTTP status code this verb produced
    body: dict[str, object] = field(default_factory=dict[str, object])  # => resource or error payload
 
 
STORE: dict[int, str] = {1: "Write tests", 2: "Ship feature"}  # => co-01: the in-memory /tasks store
NEXT_ID = [3]  # => a mutable counter cell -- the next id a POST will mint
 
 
def handle_get() -> Response:  # => GET /tasks -- READ, safe + idempotent (co-01)
    return Response(status=200, body={"tasks": {i: t for i, t in STORE.items()}})  # => returns the whole collection
 
 
def handle_post(title: str) -> Response:  # => POST /tasks -- CREATE, neither safe nor idempotent (co-01)
    new_id = NEXT_ID[0]  # => mints a brand-new id on every call
    STORE[new_id] = title  # => adds a NEW member to the collection
    NEXT_ID[0] += 1  # => advances the counter for the next create
    return Response(status=201, body={"id": new_id, "title": title})  # => 201 Created
 
 
def handle_put(task_id: int, title: str) -> Response:  # => PUT /tasks/{id} -- REPLACE, idempotent (co-01)
    if task_id not in STORE:  # => no such resource -> 404, PUT to a missing id cannot invent it here
        return Response(status=404, body={"error": "not found"})  # => 404
    STORE[task_id] = title  # => OVERWRITES the resource with the given representation
    return Response(status=200, body={"id": task_id, "title": title})  # => 200, replaced
 
 
def handle_delete(task_id: int) -> Response:  # => DELETE /tasks/{id} -- REMOVE, idempotent (co-01)
    if task_id not in STORE:  # => already gone -> 404
        return Response(status=404, body={"error": "not found"})  # => 404
    del STORE[task_id]  # => removes the member from the collection
    return Response(status=204, body={})  # => 204 No Content
 
 
get = handle_get()  # => GET: reads the collection, no side effect
print(f"GET    -> status={get.status}, tasks={get.body['tasks']}")  # => Output: 200, both seeded tasks
 
created = handle_post("Review PR")  # => POST: creates a new task
print(f"POST   -> status={created.status}, created={created.body}")  # => Output: 201, id=3
 
replaced = handle_put(1, "Write MORE tests")  # => PUT: replaces task 1's title
print(f"PUT    -> status={replaced.status}, replaced={replaced.body}")  # => Output: 200, new title
 
deleted = handle_delete(2)  # => DELETE: removes task 2
print(f"DELETE -> status={deleted.status}, body={deleted.body}")  # => Output: 204, empty body
 
print(f"final store: {STORE}")  # => Output: task 1 replaced, task 2 gone, task 3 added

Run: python3 example.py

Output:

GET    -> status=200, tasks={1: 'Write tests', 2: 'Ship feature'}
POST   -> status=201, created={'id': 3, 'title': 'Review PR'}
PUT    -> status=200, replaced={'id': 1, 'title': 'Write MORE tests'}
DELETE -> status=204, body={}
final store: {1: 'Write MORE tests', 3: 'Review PR'}

Key takeaway: GET reads, POST creates, PUT replaces, DELETE removes -- the verb carries the action, the path names the thing.

Why it matters: Every status-code, idempotency, and versioning example in this course assumes this CRUD-to-verb mapping; getting it right once means every later example can simply name a verb.


Example 2: Safe vs Idempotent -- Repeat GET, PUT, POST

ex-02 · exercises co-01

RFC 9110 marks GET safe+idempotent and PUT idempotent, but POST neither. Repeating the same request twice leaves GET/PUT in the same end state, while POST creates a second resource.

learning/code/ex-02-safe-vs-idempotent/example.py

# pyright: strict
"""Example 2: Safe vs Idempotent -- repeat GET, PUT, POST. (co-01)
 
RFC 9110 marks GET safe+idempotent and PUT idempotent, but POST NEITHER.
Calling the SAME request twice: GET and PUT reach the SAME end state, while
POST creates a SECOND resource. Source: RFC 9110 Sec 9.2.1-9.2.2 (STD 97).
"""
 
from dataclasses import dataclass  # => a small typed record for each call's footprint
 
 
@dataclass(frozen=True)  # => frozen: one row of the RFC 9110 method table, never mutated
class MethodFact:  # => co-01: the safe/idempotent classification for one method
    method: str  # => the HTTP method this row documents
    safe: bool  # => read-only -- no server state change (GET/HEAD/OPTIONS/TRACE)
    idempotent: bool  # => repeatable -- N calls == 1 call's end state (GET/PUT/DELETE/...)
 
 
RFC9110_METHODS: tuple[MethodFact, ...] = (  # => co-01: the authoritative table from RFC 9110
    MethodFact("GET", safe=True, idempotent=True),  # => read-only, repeatable
    MethodFact("PUT", safe=False, idempotent=True),  # => writes, but repeatable to the same end state
    MethodFact("POST", safe=False, idempotent=False),  # => writes AND each call creates anew
    MethodFact("DELETE", safe=False, idempotent=True),  # => removes, repeatable (second delete is a no-op)
)  # => end of the RFC 9110 method table
 
STORE: dict[int, str] = {}  # => the in-memory resource store, reset between demos
NEXT_ID = [1]  # => a mutable counter cell
 
 
def get(item_id: int) -> str:  # => GET: returns the value WITHOUT mutating STORE
    return STORE.get(item_id, "<missing>")  # => co-01: safe -- no side effect
 
 
def put(item_id: int, value: str) -> None:  # => PUT: SETS this exact key -- repeatable
    STORE[item_id] = value  # => co-01: idempotent -- two identical PUTs leave STORE identical
 
 
def post(value: str) -> int:  # => POST: mints a NEW id every call -- NOT repeatable
    new_id = NEXT_ID[0]  # => a fresh id each time
    STORE[new_id] = value  # => a brand-new entry
    NEXT_ID[0] += 1  # => advances the counter so the NEXT call differs
    return new_id
 
 
# GET twice: safe+idempotent -- STORE never changes.
STORE[1] = "v1"  # => seed one item so GET has something to read
get(1); get(1)  # => two reads, zero writes -- co-01: safe
print(f"GET twice, store unchanged: {STORE}")  # => Output: {1: 'v1'}
 
# PUT twice: idempotent -- same key set to the same value twice == one end state.
put(1, "v2"); put(1, "v2")  # => co-01: two identical PUTs collapse to ONE final value
print(f"PUT twice, one final value: {STORE}")  # => Output: {1: 'v2'}
 
# POST twice: NOT idempotent -- two resources created.
STORE.clear(); NEXT_ID[0] = 1  # => reset for a clean POST demo
post("dup"); post("dup")  # => co-01: SAME body, TWO different ids
print(f"POST twice, two resources: {STORE}")  # => Output: {1: 'dup', 2: 'dup'}
 
# Mechanically confirm the table: POST is the only method here that is NOT idempotent.
non_idempotent = [m.method for m in RFC9110_METHODS if not m.idempotent]  # => co-01
print(f"methods that are NOT idempotent: {non_idempotent}")  # => Output: ['POST']

Run: python3 example.py

Output:

GET twice, store unchanged: {1: 'v1'}
PUT twice, one final value: {1: 'v2'}
POST twice, two resources: {1: 'dup', 2: 'dup'}
methods that are NOT idempotent: ['POST']

Key takeaway: Idempotency is about the RESULT of repeating a request, not whether the method has side effects -- both PUT and POST write data, but only PUT's repeat leaves the store unchanged.

Why it matters: A client that auto-retries on a flaky network needs to know whether retrying is safe -- retrying PUT always is, blindly retrying POST can duplicate resources, which is exactly the gap the Idempotency-Key (co-06) closes.


Example 3: 201 Created + Location

ex-03 · exercises co-02

A successful POST that creates a resource returns 201 Created AND a Location header pointing at the new resource -- the status alone never says WHERE the new thing lives.

learning/code/ex-03-status-201-created/example.py

# pyright: strict
"""Example 3: 201 Created + Location. (co-02)
 
A successful POST that creates a resource returns 201 Created AND a Location
header pointing at the new resource -- the status alone never says WHERE the
new thing lives; the header does. Source: RFC 9110 Sec 15.3.2.
"""
 
from dataclasses import dataclass, field  # => field: gives the headers dict its own factory
 
 
@dataclass  # => co-02: the two facts a caller needs -- status, and where the resource landed
class Response:
    status: int  # => the HTTP status code
    headers: dict[str, str] = field(default_factory=dict[str, str])  # => response headers (Location lives here)
    body: dict[str, object] = field(default_factory=dict[str, object])  # => the created resource representation
 
 
STORE: dict[int, dict[str, object]] = {}  # => in-memory resource store, keyed by id
NEXT_ID = [1]  # => a mutable counter cell -- the next id a create will mint
 
 
def create_task(title: str) -> Response:  # => POST /tasks -- creates a new subordinate resource
    new_id = NEXT_ID[0]  # => mints a fresh id for this new resource
    resource: dict[str, object] = {"id": new_id, "title": title}  # => the created representation
    STORE[new_id] = resource  # => persists it so a follow-up GET could find it
    NEXT_ID[0] += 1  # => advances the counter for the next create
    location = f"/tasks/{new_id}"  # => co-02: the URI of the JUST-CREATED resource
    return Response(status=201, headers={"Location": location}, body=resource)  # => 201 + Location, co-02's pair
 
 
response = create_task("Deploy to staging")  # => run the handler once
print(f"status={response.status}")  # => Output: 201
print(f"Location={response.headers['Location']}")  # => Output: /tasks/1
assert response.status == 201  # => co-02: confirms the status half of the contract
assert response.headers["Location"] == "/tasks/1"  # => confirms the Location half
print(f"created: {response.body}")  # => Output: the created representation, echoed back

Run: python3 example.py

Output:

status=201
Location=/tasks/1
created: {'id': 1, 'title': 'Deploy to staging'}

Key takeaway: 201 alone tells a client 'something new exists' but not what to do next -- the Location header turns that into an actionable follow-up GET.

Why it matters: A client that ignores Location and constructs its own URL is fragile the moment the server-assigned id diverges from anything the client sent.


Example 4: 204 No Content for DELETE

ex-04 · exercises co-02

A successful DELETE has nothing left to represent, so RFC 9110 gives it 204 No Content: success, but an intentionally EMPTY body.

learning/code/ex-04-status-204-delete/example.py

# pyright: strict
"""Example 4: 204 No Content for DELETE. (co-02)
 
A successful DELETE has nothing left to represent -- the resource is gone --
so RFC 9110 gives it 204 No Content: success, but an intentionally EMPTY
body, distinct from 200 OK's "success, and here is a representation."
"""
 
from dataclasses import dataclass  # => a small typed response record for this example
 
 
@dataclass  # => co-02: status plus a body kept as a plain str for a trivial emptiness check
class Response:
    status: int  # => the HTTP status code
    body: str  # => kept as a plain str so an EMPTY body is trivially checkable ("" == empty)
 
 
STORE: dict[int, str] = {1: "Draft task", 2: "Another task"}  # => two seeded resources to delete from
 
 
def delete_task(task_id: int) -> Response:  # => DELETE /tasks/{id}
    if task_id not in STORE:  # => no such resource -- nothing to delete
        return Response(status=404, body="not found")  # => 404, the resource never existed
    del STORE[task_id]  # => co-02: the resource is now genuinely gone from the store
    return Response(status=204, body="")  # => co-02: 204 -- success, deliberately empty body
 
 
response = delete_task(1)  # => delete the one seeded task
print(f"status={response.status}, body={response.body!r}")  # => Output: status=204, body=''
assert response.status == 204  # => co-02: confirms the "no content" status
assert response.body == ""  # => confirms the body is genuinely empty, not e.g. "null"
assert 1 not in STORE  # => confirms the underlying resource really was removed
print(f"remaining in store: {STORE}")  # => Output: only task 2 remains

Run: python3 example.py

Output:

status=204, body=''
remaining in store: {2: 'Another task'}

Key takeaway: 204's empty body is a deliberate design choice -- a client should never try to json.loads() a 204 body.

Why it matters: Returning 200 with a null body for a DELETE forces every client to special-case parsing an 'empty-ish' body; 204 removes that ambiguity at the protocol level.


Example 5: 400 Bad Request -- a Malformed Payload

ex-05 · exercises co-02

A syntactically malformed request body (wrong type, missing required field, empty) is rejected with 400 Bad Request -- the server never reaches business logic.

learning/code/ex-05-status-400-validation/example.py

# pyright: strict
"""Example 5: 400 Bad Request -- a malformed payload. (co-02)
 
A syntactically malformed request body (wrong type, missing required field)
is rejected with 400 Bad Request -- the server never reaches business logic
because the payload could not be parsed into the expected shape.
"""
 
from dataclasses import dataclass  # => a small typed response record for this example
 
 
@dataclass  # => co-02: status plus a small error body
class Response:
    status: int  # => the HTTP status code
    body: dict[str, str]  # => either an error message or the accepted payload
 
 
def parse_task_payload(raw: dict[str, object]) -> tuple[Response, str | None]:  # => returns (response, parsed title)
    # => co-02: validate BEFORE touching the store -- a malformed body is a 400, not a business error
    if "title" not in raw:  # => the required field is missing entirely
        return Response(400, {"error": "missing required field: title"}), None  # => 400, malformed
    title_value = raw["title"]  # => the value present at the title key
    if not isinstance(title_value, str):  # => the title is present but the WRONG TYPE (e.g. an int)
        return Response(400, {"error": "title must be a string"}), None  # => 400, malformed
    if title_value == "":  # => present and a string, but empty -- still malformed for this endpoint
        return Response(400, {"error": "title must not be empty"}), None  # => 400, malformed
    return Response(202, {"accepted": title_value}), title_value  # => accepted shape (202 just signals "ok" here)
 
 
missing = parse_task_payload({})  # => no title key at all
print(f"missing title:    status={missing[0].status}, body={missing[0].body}")  # => Output: 400
 
wrong_type = parse_task_payload({"title": 42})  # => title present but not a string
print(f"wrong type:       status={wrong_type[0].status}, body={wrong_type[0].body}")  # => Output: 400
 
empty = parse_task_payload({"title": ""})  # => title present, a string, but empty
print(f"empty title:      status={empty[0].status}, body={empty[0].body}")  # => Output: 400
 
ok = parse_task_payload({"title": "Valid task"})  # => a well-formed payload
print(f"well-formed:      status={ok[0].status}, body={ok[0].body}")  # => Output: 202, accepted
 
assert missing[0].status == 400 and wrong_type[0].status == 400 and empty[0].status == 400  # => co-02: all three are 400

Run: python3 example.py

Output:

missing title:    status=400, body={'error': 'missing required field: title'}
wrong type:       status=400, body={'error': 'title must be a string'}
empty title:      status=400, body={'error': 'title must not be empty'}
well-formed:      status=202, body={'accepted': 'Valid task'}

Key takeaway: 400 is about the payload's SHAPE being unparseable, distinct from 422's 'parses but semantically invalid'.

Why it matters: Collapsing every rejection into 400 forces a client to string-match error messages; 400 vs 422 vs 409 let a client branch its remediation without parsing prose.


Example 6: 401 Unauthorized vs 403 Forbidden

ex-06 · exercises co-02

401 means authentication required and has failed or not been provided; 403 means the server refuses to authorize a VALID identity that lacks permission. The two are distinct.

learning/code/ex-06-status-401-vs-403/example.py

# pyright: strict
"""Example 6: 401 Unauthorized vs 403 Forbidden. (co-02)
 
401 = authentication required and has failed or not been provided; 403 = the
server refuses to authorize a VALID identity that lacks permission. The two
are distinct: 401 is "who are you?", 403 is "I know who you are, but no."
Source: RFC 9110 Sec 15.5.1 (401) and Sec 15.5.4 (403).
"""
 
from dataclasses import dataclass  # => a small typed response record for this example
 
 
@dataclass  # => co-02: status plus an error message
class Response:
    status: int  # => the HTTP status code
    body: dict[str, str]  # => a short error explanation
 
 
# A toy token store: token -> (user, role). "admin" can delete; "viewer" cannot.
TOKENS: dict[str, tuple[str, str]] = {"tok-admin": ("ada", "admin"), "tok-viewer": ("grace", "viewer")}
REQUIRED_ROLE = "admin"  # => the role this protected operation demands
 
 
def delete_task(token: str | None, task_id: int) -> Response:  # => DELETE /tasks/{id}, auth-gated
    if token is None or token not in TOKENS:  # => co-02: no identity provided OR identity not recognized -> 401
        return Response(401, {"error": "authentication required"})  # => 401: "who are you?"
    _user, role = TOKENS[token]  # => identity is valid -- now check authorization
    if role != REQUIRED_ROLE:  # => co-02: valid identity, but lacks the required role -> 403
        return Response(403, {"error": "insufficient role"})  # => 403: "I know you, but no."
    return Response(204, {"deleted": str(task_id)})  # => authenticated AND authorized -> 204
 
 
no_token = delete_task(token=None, task_id=1)  # => no Authorization header at all
print(f"no token:     status={no_token.status}, body={no_token.body}")  # => Output: 401
 
bad_token = delete_task(token="tok-bogus", task_id=1)  # => a token the server does not recognize
print(f"bad token:    status={bad_token.status}, body={bad_token.body}")  # => Output: 401
 
wrong_role = delete_task(token="tok-viewer", task_id=1)  # => valid identity, but role "viewer" cannot delete
print(f"wrong role:   status={wrong_role.status}, body={wrong_role.body}")  # => Output: 403
 
ok = delete_task(token="tok-admin", task_id=1)  # => valid identity, correct role
print(f"admin:        status={ok.status}, body={ok.body}")  # => Output: 204
 
assert no_token.status == 401 and bad_token.status == 401  # => co-02: auth-missing is 401
assert wrong_role.status == 403  # => co-02: valid-auth-but-forbidden is 403
assert ok.status == 204  # => authorized

Run: python3 example.py

Output:

no token:     status=401, body={'error': 'authentication required'}
bad token:    status=401, body={'error': 'authentication required'}
wrong role:   status=403, body={'error': 'insufficient role'}
admin:        status=204, body={'deleted': '1'}

Key takeaway: 401 is 'who are you?' and 403 is 'I know who you are, but no' -- conflating them misleads every client's retry and re-auth logic.

Why it matters: A valid token that lacks a role must return 403, not 401 -- otherwise a client wrongly concludes its credentials are bad and re-prompts the user forever.


Example 7: 409 Conflict -- a Duplicate Create

ex-07 · exercises co-02

409 Conflict means the request collides with the resource's CURRENT state -- here, a username that already exists.

learning/code/ex-07-status-409-conflict/example.py

# pyright: strict
"""Example 7: 409 Conflict -- a duplicate create. (co-02)
 
409 Conflict means the request collides with the resource's CURRENT state --
here, creating a username that already exists. The request itself is well-
formed, but applying it right now would duplicate a unique value. Source:
RFC 9110 Sec 15.5.10.
"""
 
from dataclasses import dataclass  # => a small typed response record for this example
 
 
@dataclass  # => co-02: status plus an error or success body
class Response:
    status: int  # => the HTTP status code
    body: dict[str, str]  # => either an error message or the created resource
 
 
USERNAMES: set[str] = {"ada"}  # => the "current state" a 409 uniqueness check compares against
 
 
def create_user(username: str) -> Response:  # => POST /users -- can succeed or collide
    if username in USERNAMES:  # => co-02: collides with EXISTING state -- a conflict, not a syntax error
        return Response(409, {"error": f"username {username!r} already taken"})  # => 409, state clash
    USERNAMES.add(username)  # => no collision -- the write actually happens
    return Response(201, {"username": username})  # => success, resource created
 
 
conflict = create_user("ada")  # => "ada" already exists -> conflict
print(f"duplicate: status={conflict.status}, body={conflict.body}")  # => Output: 409
 
created = create_user("grace")  # => a brand-new username -> succeeds
print(f"new user:  status={created.status}, body={created.body}")  # => Output: 201
 
second_conflict = create_user("grace")  # => "grace" now also exists -> conflict
print(f"duplicate: status={second_conflict.status}, body={second_conflict.body}")  # => Output: 409
 
assert conflict.status == 409 and second_conflict.status == 409  # => co-02: both duplicates are 409
assert created.status == 201  # => the genuinely new one succeeded

Run: python3 example.py

Output:

duplicate: status=409, body={'error': "username 'ada' already taken"}
new user:  status=201, body={'username': 'grace'}
duplicate: status=409, body={'error': "username 'grace' already taken"}

Key takeaway: 409 is a collision with EXISTING state; the request itself is well-formed, but applying it now would duplicate a unique value.

Why it matters: A client seeing 409 knows to retry with a different value, whereas 422 means 'fix this field' -- genuinely different remediation paths.


Example 8: 422 Unprocessable Content

ex-08 · exercises co-02

RFC 9110 Sec 15.5.21 defines 422 'Unprocessable Content' (renamed from RFC 4918's 'Unprocessable Entity'). The JSON parses fine, but a field fails a business rule.

learning/code/ex-08-status-422-unprocessable/example.py

# pyright: strict
"""Example 8: 422 Unprocessable Content -- well-formed but semantically invalid. (co-02)
 
RFC 9110 Sec 15.5.21 defines 422 "Unprocessable Content" (renamed from
RFC 4918's WebDAV-era "Unprocessable Entity"). The JSON parses fine, but a
field fails a BUSINESS rule -- here, a negative age. RFC 9110 does NOT
obsolete RFC 4918; they coexist. Teach "Unprocessable Content".
"""
 
from dataclasses import dataclass  # => a small typed response record for this example
 
 
@dataclass  # => co-02: status plus an error or success body
class Response:
    status: int  # => the HTTP status code
    body: dict[str, object]  # => either a field error or the created resource
 
 
def create_user(username: str, age: int) -> Response:  # => POST /users -- syntactically valid, semantically checked
    if age < 0:  # => co-02: a valid integer, but a nonsensical VALUE -- semantic, not syntactic
        return Response(422, {"error": "age must be non-negative", "field": "age"})  # => 422, semantic failure
    return Response(201, {"username": username, "age": age})  # => success, resource created
 
 
negative_age = create_user("grace", -5)  # => a well-formed body, but a semantically invalid age
print(f"negative age: status={negative_age.status}, body={negative_age.body}")  # => Output: 422
 
valid = create_user("grace", 37)  # => a fully valid input
print(f"valid:        status={valid.status}, body={valid.body}")  # => Output: 201
 
# Contrast with a 400 (Example 5): here the JSON parsed fine -- the failure is the VALUE's meaning.
assert negative_age.status == 422  # => co-02: semantic failure is 422, not 400
assert valid.status == 201  # => a valid value succeeds

Run: python3 example.py

Output:

negative age: status=422, body={'error': 'age must be non-negative', 'field': 'age'}
valid:        status=201, body={'username': 'grace', 'age': 37}

Key takeaway: 422 is a well-formed body with a semantically invalid VALUE -- distinct from 400's malformed shape.

Why it matters: Teaching 'Unprocessable Content' (not the older 'Entity') keeps the course aligned with the current RFC 9110 wording.


Example 9: Versioning via the URI Path

ex-09 · exercises co-03

Routing the version into the URI path (/v1/tasks vs /v2/tasks) is the strategy Google's AIP-185 names. The version lives in the URL, so a CDN naturally treats each version as distinct.

learning/code/ex-09-version-uri-path/example.py

# pyright: strict
"""Example 9: Versioning via the URI Path -- /v1/ vs /v2/. (co-03)
 
Routing the version into the URI path (/v1/tasks vs /v2/tasks) is the
strategy Google's AIP-185 names ("major version in the URI, e.g. v1 not
v1.0"). The version lives in the URL, so an HTTP cache or CDN naturally
treats each version as a distinct entry with no special config.
"""
 
from collections.abc import Callable  # => Callable: the type of the version-specific handlers
from dataclasses import dataclass  # => a small typed response record for each version
 
 
@dataclass  # => co-03: one version's own response shape -- v1 and v2 differ deliberately
class Response:
    status: int  # => the HTTP status code
    body: dict[str, object]  # => the version-specific representation
 
 
def tasks_v1() -> Response:  # => /v1/tasks -- the original representation (title only)
    return Response(200, {"version": 1, "tasks": [{"id": 1, "title": "Legacy task"}]})  # => co-03: v1 shape
 
 
def tasks_v2() -> Response:  # => /v2/tasks -- the evolved representation (title + status added)
    return Response(200, {"version": 2, "tasks": [{"id": 1, "title": "Legacy task", "status": "open"}]})  # => co-03: v2 adds a field
 
 
ROUTES: dict[str, Callable[[], Response]] = {"/v1/tasks": tasks_v1, "/v2/tasks": tasks_v2}  # => co-03: version baked INTO the path
 
 
def route(path: str) -> Response:  # => a tiny path-based router
    handler = ROUTES.get(path)  # => looks up the handler for this exact versioned path
    if handler is None:  # => unknown path or unknown version -> 404
        return Response(404, {"error": f"no route for {path}"})  # => 404
    return handler()  # => invokes the version-specific handler
 
 
v1 = route("/v1/tasks")  # => resolves to the v1 handler
print(f"/v1/tasks -> status={v1.status}, body={v1.body}")  # => Output: version 1, title only
 
v2 = route("/v2/tasks")  # => resolves to the v2 handler
print(f"/v2/tasks -> status={v2.status}, body={v2.body}")  # => Output: version 2, title + status
 
missing = route("/v3/tasks")  # => a version that was never published -> 404
print(f"/v3/tasks -> status={missing.status}, body={missing.body}")  # => Output: 404
 
assert v1.body["version"] == 1 and v2.body["version"] == 2  # => co-03: each version resolves to its own handler

Run: python3 example.py

Output:

/v1/tasks -> status=200, body={'version': 1, 'tasks': [{'id': 1, 'title': 'Legacy task'}]}
/v2/tasks -> status=200, body={'version': 2, 'tasks': [{'id': 1, 'title': 'Legacy task', 'status': 'open'}]}
/v3/tasks -> status=404, body={'error': 'no route for /v3/tasks'}

Key takeaway: URI-path versioning bakes the version into the cache key for free, at the cost of URL churn across major versions.

Why it matters: The versioning strategy affects HTTP-cache compatibility and discoverability -- there is no single right answer, only named forces.


Example 10: Versioning via a Request Header

ex-10 · exercises co-03

Selecting the version from a request header (Stripe's Stripe-Version style) keeps the URL identical across versions but asks the client to opt into one.

learning/code/ex-10-version-header/example.py

# pyright: strict
"""Example 10: Versioning via a Request Header. (co-03)
 
Selecting the version from a request header (e.g. Stripe's `Stripe-Version`)
keeps the URL identical across versions but asks the client to opt into one.
The trade-off: a cache keyed only on the URL now needs extra configuration to
avoid conflating two versions under the same cached URL.
"""
 
from dataclasses import dataclass  # => a small typed response record for each version
 
 
@dataclass  # => co-03: one version's own response shape
class Response:
    status: int  # => the HTTP status code
    body: dict[str, object]  # => the version-specific representation
 
 
@dataclass  # => co-03: a request carries a path AND a version-selecting header
class Request:
    path: str  # => the (version-free) resource path
    headers: dict[str, str]  # => the version is selected HERE, not in the URL
 
 
def tasks_v1() -> Response:  # => the original representation
    return Response(200, {"version": 1, "tasks": [{"id": 1, "title": "Legacy task"}]})  # => v1 shape
 
 
def tasks_v2() -> Response:  # => the evolved representation (a field added)
    return Response(200, {"version": 2, "tasks": [{"id": 1, "title": "Legacy task", "status": "open"}]})  # => v2 adds status
 
 
VERSION_HEADER = "X-API-Version"  # => the header name this API selects the version with
 
 
def route(request: Request) -> Response:  # => a header-based router -- the URL is the SAME for both versions
    version = request.headers.get(VERSION_HEADER, "1")  # => defaults to v1 when the header is absent
    if version == "1":  # => co-03: the client opted into v1 (or sent nothing)
        return tasks_v1()  # => v1 handler
    if version == "2":  # => co-03: the client opted into v2
        return tasks_v2()  # => v2 handler
    return Response(404, {"error": f"unknown version {version}"})  # => an unrecognized version
 
 
default_req = Request(path="/tasks", headers={})  # => no version header -> defaults to v1
print(f"no header (default v1): version={route(default_req).body['version']}")  # => Output: 1
 
v1_req = Request(path="/tasks", headers={VERSION_HEADER: "1"})  # => explicitly v1
print(f"header=1:               version={route(v1_req).body['version']}")  # => Output: 1
 
v2_req = Request(path="/tasks", headers={VERSION_HEADER: "2"})  # => explicitly v2 -- SAME path as v1
print(f"header=2:               version={route(v2_req).body['version']}")  # => Output: 2
 
# Same URL, two versions: a cache keyed only on /tasks would conflate them -- the header-versioning trade-off.
assert route(v1_req).body["version"] == 1 and route(v2_req).body["version"] == 2  # => co-03
print(f"same path '/tasks' served versions 1 and 2 by header")  # => Output: header routing confirmed

Run: python3 example.py

Output:

no header (default v1): version=1
header=1:               version=1
header=2:               version=2
same path '/tasks' served versions 1 and 2 by header

Key takeaway: Header versioning keeps URLs clean, but a cache keyed only on the URL now needs extra configuration to avoid conflating two versions.

Why it matters: The trade-off vs URI-path is concrete: header versioning trades URL stability for cache-key complexity.


Example 11: Offset/Limit Pagination

ex-11 · exercises co-04

?offset=20&limit=10 asks the server to SKIP 20 rows and return the NEXT 10. This example builds the slicing and verifies the correct window returns.

learning/code/ex-11-offset-limit-page/example.py

# pyright: strict
"""Example 11: Offset/Limit Pagination -- the correct slice. (co-04)
 
?offset=20&limit=10 asks the server to SKIP 20 rows and return the NEXT 10.
This example builds the slicing and verifies the correct window (rows 21..30)
comes back. The fetch-and-discard COST of that skip is the subject of Example 12.
"""
 
from dataclasses import dataclass  # => a small typed response record for a page
 
 
@dataclass  # => co-04: a page of items plus a pointer to keep paging
class Page:
    data: list[int]  # => the items on THIS page (their ids)
    offset: int  # => the offset this page started at
    limit: int  # => the limit (count) requested
    total: int  # => the full collection size, so the caller knows if more pages exist
 
 
ROWS: list[int] = list(range(1, 101))  # => 100 rows, ids 1..100 -- the collection being paged
 
 
def page_offset_limit(offset: int, limit: int) -> Page:  # => co-04: GET /items?offset=20&limit=10
    start = offset  # => the number of rows to SKIP
    window = ROWS[start : start + limit]  # => co-04: the next `limit` rows after skipping `offset`
    return Page(data=window, offset=offset, limit=limit, total=len(ROWS))  # => the page envelope
 
 
page = page_offset_limit(offset=20, limit=10)  # => skip 20, take 10 -- rows 21..30
print(f"offset=20, limit=10 -> first={page.data[0]}, last={page.data[-1]}, count={len(page.data)}")  # => Output: 21, 30, 10
 
first_page = page_offset_limit(offset=0, limit=10)  # => the very first page
print(f"offset=0, limit=10  -> first={first_page.data[0]}, last={first_page.data[-1]}")  # => Output: 1, 10
 
assert page.data == list(range(21, 31))  # => co-04: exactly rows 21 through 30
assert len(page.data) == page.limit  # => the page respects the requested count

Run: python3 example.py

Output:

offset=20, limit=10 -> first=21, last=30, count=10
offset=0, limit=10  -> first=1, last=10

Key takeaway: Offset/limit means 'skip N, take M' -- simple, but the skip pays a fetch-and-discard cost (Example 12).

Why it matters: Understanding the offset cost is the specific motivation for cursor pagination (co-05) once a dataset grows or churns.


Example 12: Offset Pagination's Fetch-and-Discard Cost

ex-12 · exercises co-04

OFFSET makes the database 'fetch and discard' every preceding row: to serve offset=5000 it walks past 5000 rows before returning 10. The instrumented counter shows the cost grows linearly.

learning/code/ex-12-offset-cost-demo/example.py

# pyright: strict
"""Example 12: Offset Pagination's Cost -- fetch-and-discard. (co-04)
 
OFFSET makes the database "fetch and discard" every preceding row: to serve
offset=1000 it must walk past 1000 rows before returning the requested 10.
This example instruments a simulated query with a row-touch counter and shows
the touch count grows linearly with the offset. Source: "Use The Index, Luke
-- No Offset" (Markus Winand).
"""
 
ROWS_TOUCHED = [0]  # => a mutable counter cell -- how many rows the "query" scanned
 
 
def fetch_with_offset(offset: int, limit: int, table: list[int]) -> list[int]:
    ROWS_TOUCHED[0] = 0  # => reset the counter for this query
    page: list[int] = []  # => the rows actually returned
    for index, row in enumerate(table):  # => simulate the DB scanning the table in order
        ROWS_TOUCHED[0] += 1  # => EVERY row the scan passes is "touched", even skipped ones
        if index < offset:  # => co-04: rows before the offset are FETCHED then DISCARDED
            continue  # => touched but not returned -- this is the cost
        page.append(row)  # => a row that is BOTH touched AND returned
        if len(page) >= limit:  # => the page is full
            break  # => stop scanning immediately -- no extra row touched
    return page
 
 
TABLE = list(range(1, 10001))  # => 10000 rows -- a large enough table to make the cost visible
 
shallow = fetch_with_offset(offset=0, limit=10, table=TABLE)  # => page 1: touches only 10 rows
print(f"offset=0, limit=10    -> returned {len(shallow)} rows, touched {ROWS_TOUCHED[0]} rows")  # => Output: 10 touched
 
deep = fetch_with_offset(offset=1000, limit=10, table=TABLE)  # => page ~101: touches 1010 rows
print(f"offset=1000, limit=10 -> returned {len(deep)} rows, touched {ROWS_TOUCHED[0]} rows")  # => Output: 1010 touched
 
deeper = fetch_with_offset(offset=5000, limit=10, table=TABLE)  # => a deep page: touches 5010 rows
print(f"offset=5000, limit=10 -> returned {len(deeper)} rows, touched {ROWS_TOUCHED[0]} rows")  # => Output: 5010 touched
 
# The returned COUNT is always 10, but the touched count grows linearly with offset -- the fetch-and-discard cost.
assert len(shallow) == len(deep) == len(deeper) == 10  # => same page size
assert ROWS_TOUCHED[0] == 5010  # => the deep page paid for 5010 touches to return 10 rows

Run: python3 example.py

Output:

offset=0, limit=10    -> returned 10 rows, touched 10 rows
offset=1000, limit=10 -> returned 10 rows, touched 1010 rows
offset=5000, limit=10 -> returned 10 rows, touched 5010 rows

Key takeaway: The returned count is always 10, but the touched count grows linearly with the offset -- that is the fetch-and-discard cost.

Why it matters: Source: 'Use The Index, Luke -- No Offset' (Markus Winand). This cost is exactly what cursor paging avoids.


Example 13: Cursor Pagination -- Resume After a Key

ex-13 · exercises co-05

A cursor encodes a position IN THE DATA (the last-seen id) and the next page resumes with WHERE id > cursor -- Stripe calls this starting_after.

learning/code/ex-13-cursor-page/example.py

# pyright: strict
"""Example 13: Cursor Pagination -- resume after a key. (co-05)
 
A cursor encodes a position IN THE DATA (the last-seen item's sort key) and
the next page resumes with `WHERE id > cursor`. This avoids the fetch-and-
discard cost and is stable under concurrent inserts. Stripe's API calls this
`starting_after`. Source: Stripe pagination docs; "Use The Index, Luke".
"""
 
from dataclasses import dataclass  # => a small typed response record for a page
 
 
@dataclass  # => co-05: a page of items plus the cursor to fetch the next page
class Page:
    data: list[int]  # => the ids on THIS page
    next_cursor: int | None  # => the last id on this page, or None when the end is reached
 
 
ROWS: list[int] = list(range(1, 101))  # => 100 rows, ids 1..100, sorted ascending (the indexed key)
 
 
def page_starting_after(starting_after: int | None, limit: int) -> Page:  # => co-05: GET /items?starting_after=<id>
    threshold = 0 if starting_after is None else starting_after  # => None means "from the beginning"
    window = [r for r in ROWS if r > threshold][:limit]  # => co-05: an indexed WHERE on the last-seen key, then LIMIT
    next_cursor = window[-1] if len(window) == limit and window[-1] != ROWS[-1] else None  # => None at the very end
    return Page(data=window, next_cursor=next_cursor)  # => the page + the cursor the NEXT request uses
 
 
first = page_starting_after(starting_after=None, limit=10)  # => page 1: ids 1..10
print(f"page 1: first={first.data[0]}, last={first.data[-1]}, next_cursor={first.next_cursor}")  # => Output: 1, 10, 10
 
second = page_starting_after(starting_after=first.next_cursor, limit=10)  # => resumes right after id 10
print(f"page 2: first={second.data[0]}, last={second.data[-1]}, next_cursor={second.next_cursor}")  # => Output: 11, 20, 20
 
assert second.data[0] == 11  # => co-05: the next page begins right AFTER the cursor id, no skip cost
assert first.data + second.data == list(range(1, 21))  # => contiguous, no overlap and no gap

Run: python3 example.py

Output:

page 1: first=1, last=10, next_cursor=10
page 2: first=11, last=20, next_cursor=20

Key takeaway: A cursor resumes from a position in the data, not a raw row count -- so the next page begins right after the cursor id.

Why it matters: Keyset paging avoids the fetch-and-discard cost and is the correct default for a production list endpoint.


Example 14: Cursor Stability Under Concurrent Insert

ex-14 · exercises co-05

Because a cursor anchors to a position IN THE DATA, it is unaffected by a row inserted before the next page. An equivalent OFFSET page drifts -- it skips or repeats a row.

learning/code/ex-14-cursor-stable-under-insert/example.py

# pyright: strict
"""Example 14: Cursor Stability Under Concurrent Insert. (co-05)
 
Because a cursor anchors to a position IN THE DATA (the last-seen id), it is
unaffected by a row inserted BEFORE the next page. An equivalent OFFSET page
DRIFTS: a new row near the front shifts every later position, so the offset
page either skips or repeats a row. Source: "Use The Index, Luke -- No Offset".
"""
 
ROWS = list(range(1, 11))  # => 10 rows, ids 1..10 (mutated below by a mid-scan insert)
 
 
def page_cursor(rows: list[int], starting_after: int | None, limit: int) -> list[int]:
    threshold = 0 if starting_after is None else starting_after  # => resume point IN the data
    return [r for r in rows if r > threshold][:limit]  # => co-05: WHERE id > cursor, stable under inserts
 
 
def page_offset(rows: list[int], offset: int, limit: int) -> list[int]:
    return rows[offset : offset + limit]  # => co-04: raw position -- drifts if rows shift
 
 
# Take the first page (ids 1..3), establishing a cursor/offset at id 3.
first_cursor = page_cursor(ROWS, None, 3)  # => [1, 2, 3]
first_offset = page_offset(ROWS, 0, 3)  # => [1, 2, 3]
print(f"page 1 (cursor): {first_cursor}")  # => Output: [1, 2, 3]
print(f"page 1 (offset): {first_offset}")  # => Output: [1, 2, 3]
 
# A new row is inserted at the FRONT of the list mid-scan (simulating a concurrent insert).
ROWS.insert(0, 0)  # => now [0, 1, 2, ..., 10] -- a row landed near the front
 
next_cursor = page_cursor(ROWS, starting_after=3, limit=3)  # => co-05: WHERE id > 3 -> [4, 5, 6] (unaffected)
next_offset = page_offset(ROWS, offset=3, limit=3)  # => co-04: position 3 shifted -> [3, 4, 5] (REPEATS id 3)
print(f"page 2 (cursor), after insert: {next_cursor}")  # => Output: [4, 5, 6] -- stable, no repeat
print(f"page 2 (offset), after insert: {next_offset}")  # => Output: [3, 4, 5] -- DRIFTED, id 3 repeated
 
assert next_cursor == [4, 5, 6]  # => co-05: cursor page is unaffected by the insert
assert 3 in next_offset and 3 not in next_cursor  # => co-04: offset page repeated id 3; cursor did not

Run: python3 example.py

Output:

page 1 (cursor): [1, 2, 3]
page 1 (offset): [1, 2, 3]
page 2 (cursor), after insert: [4, 5, 6]
page 2 (offset), after insert: [3, 4, 5]

Key takeaway: A cursor is stable under concurrent writes; an offset page drifts because a new row shifts every later position.

Why it matters: This drift is the concrete reason cursor pagination wins at any real scale where inserts happen mid-scan.


Example 15: Idempotency-Key -- Store and Replay

ex-15 · exercises co-06

On a first POST the server records key -> response. A REPLAY of the same key returns the ORIGINAL stored response (200) instead of creating a second resource -- Stripe prior art; the IETF draft lapsed.

learning/code/ex-15-idempotency-key-store/example.py

# pyright: strict
"""Example 15: Idempotency-Key -- store and replay. (co-06)
 
On a first POST the server records key -> response. A REPLAY of the same key
returns the ORIGINAL stored response (200) instead of creating a second
resource. This is Stripe's own prior art: store the first result under the
key, replay returns it -- even a 500. NOTE: the IETF draft
(draft-ietf-httpapi-idempotency-key-header-07) is EXPIRED/lapsed, not an RFC,
so this example follows Stripe prior art, not a lapsed draft.
"""
 
from dataclasses import dataclass, field  # => field: mutable-default-safe factories
 
 
@dataclass  # => co-06: the response shape stored and replayed
class Response:
    status: int  # => 201 on first create, 200 on replay
    body: dict[str, object] = field(default_factory=dict[str, object])  # => the resource representation
 
 
STORE: dict[int, dict[str, object]] = {}  # => the resource store
IDEMPOTENCY_STORE: dict[str, dict[str, object]] = {}  # => co-06: key -> the BODY the first call produced
NEXT_ID = [1]  # => a mutable counter cell
 
 
def create_order(idempotency_key: str, item: str) -> Response:  # => POST /orders -- idempotent via key
    if idempotency_key in IDEMPOTENCY_STORE:  # => co-06: a REPLAY -- do NOT create a second order
        return Response(status=200, body=IDEMPOTENCY_STORE[idempotency_key])  # => returns the ORIGINAL body, 200 semantics
    new_id = NEXT_ID[0]  # => a fresh id for a genuinely new order
    order: dict[str, object] = {"id": new_id, "item": item}  # => the new resource
    STORE[new_id] = order  # => persists it
    IDEMPOTENCY_STORE[idempotency_key] = order  # => co-06: recorded so a future replay is safe
    NEXT_ID[0] += 1  # => advances the counter for the next genuinely new order
    return Response(status=201, body=order)  # => 201 on first create
 
 
first = create_order("client-key-A", "Widget")  # => a genuinely new key -> creates
print(f"first call:  status={first.status}, body={first.body}")  # => Output: 201, id=1
 
replay = create_order("client-key-A", "Widget")  # => co-06: SAME key -> returns the ORIGINAL response
print(f"replay call: status={replay.status}, body={replay.body}")  # => Output: 200, SAME id=1
 
assert first.status == 201 and replay.status == 200  # => co-06: first 201, replay 200
assert first.body == replay.body  # => the replay returns the IDENTICAL response
assert len(STORE) == 1  # => only ONE order was created across both calls

Run: python3 example.py

Output:

first call:  status=201, body={'id': 1, 'item': 'Widget'}
replay call: status=200, body={'id': 1, 'item': 'Widget'}

Key takeaway: A known key short-circuits to the ORIGINAL response, so a retried POST creates nothing new.

Why it matters: This is the explicit mechanism that makes a non-idempotent method (POST) safely retryable.


Example 16: Idempotency-Key -- a Retried Charge Applies Once

ex-16 · exercises co-06

A retried POST charge with the SAME Idempotency-Key must not double-charge. The first call debits and records the key; a replay returns the original result WITHOUT debiting again.

learning/code/ex-16-idempotency-key-no-double-apply/example.py

# pyright: strict
"""Example 16: Idempotency-Key -- a retried charge applies ONCE. (co-06)
 
A retried POST charge with the SAME Idempotency-Key must not double-charge.
This example models a wallet balance: the first call debits and records the
key; a replay returns the original result WITHOUT debiting again.
"""
 
from dataclasses import dataclass, field  # => field: mutable-default-safe factories
 
 
@dataclass  # => co-06: the charge result stored and replayed
class ChargeResult:
    status: int  # => 201 first, 200 replay
    body: dict[str, object] = field(default_factory=dict[str, object])  # => charge id + new balance
 
 
BALANCE = [1000]  # => the wallet, mutable so a debit persists across calls
CHARGES: dict[str, dict[str, object]] = {}  # => co-06: idempotency key -> the charge BODY
 
 
def charge(idempotency_key: str, amount: int) -> ChargeResult:  # => POST /charges -- debit the wallet
    if idempotency_key in CHARGES:  # => co-06: a REPLAY -> return original body, do NOT debit again
        return ChargeResult(status=200, body=CHARGES[idempotency_key])  # => the original result, untouched
    BALANCE[0] -= amount  # => genuinely new charge -> debit the wallet once
    body: dict[str, object] = {"charged": amount, "balance": BALANCE[0]}  # => the result body
    CHARGES[idempotency_key] = body  # => co-06: recorded so a replay is a no-op
    return ChargeResult(status=201, body=body)  # => 201 on the genuine charge
 
 
first = charge("pay-key-1", 250)  # => genuine charge: 1000 -> 750
print(f"first charge:  status={first.status}, body={first.body}")  # => Output: 201, balance 750
 
replay = charge("pay-key-1", 250)  # => co-06: retry after a timeout -- SAME key, no second debit
print(f"replay charge: status={replay.status}, body={replay.body}")  # => Output: 200, balance still 750
 
second = charge("pay-key-2", 100)  # => a DIFFERENT key -> a genuinely new charge
print(f"second charge: status={second.status}, body={second.body}")  # => Output: 201, balance 650
 
assert BALANCE[0] == 650  # => co-06: debited exactly 250 + 100 = 350, the replay did NOT double-charge
assert first.body["balance"] == replay.body["balance"]  # => replay returned the original result unchanged

Run: python3 example.py

Output:

first charge:  status=201, body={'charged': 250, 'balance': 750}
replay charge: status=200, body={'charged': 250, 'balance': 750}
second charge: status=201, body={'charged': 100, 'balance': 650}

Key takeaway: The balance is debited exactly once across the original charge and its replay -- no double-apply.

Why it matters: Idempotency keys came from payments (a retried charge must not double-bill), the canonical motivating use case.


Example 17: Idempotency-Key Mismatch -- Reused with a Different Body

ex-17 · exercises co-06

Reusing a known key with a DIFFERENT request body is a client bug. The server REJECTS it rather than silently returning the stale response or applying the new body.

learning/code/ex-17-idempotency-key-mismatch/example.py

# pyright: strict
"""Example 17: Idempotency-Key Mismatch -- reuse a key with a different body. (co-06)
 
Reusing a known key with a DIFFERENT request body is a client bug (not a safe
retry). The server REJECTS it rather than silently returning the stale
response or applying the new body. This prevents a key collision from doing
the wrong thing on either side.
"""
 
from dataclasses import dataclass, field  # => field: mutable-default-safe factories
 
 
@dataclass  # => co-06: status plus body or error
class Response:
    status: int  # => the HTTP status code
    body: dict[str, object] = field(default_factory=dict[str, object])  # => resource or error payload
 
 
STORE: dict[int, dict[str, object]] = {}  # => the resource store
IDEMPOTENCY_STORE: dict[str, tuple[str, Response]] = {}  # => co-06: key -> (request body hash, response)
NEXT_ID = [1]  # => a mutable counter cell
 
 
def create_order(idempotency_key: str, item: str) -> Response:  # => POST /orders -- idempotent + mismatch-checked
    if idempotency_key in IDEMPOTENCY_STORE:  # => key was seen before -- inspect whether the body matches
        seen_body, stored_response = IDEMPOTENCY_STORE[idempotency_key]  # => the original body + response
        if seen_body != item:  # => co-06: SAME key, DIFFERENT body -> client bug, REJECT
            return Response(409, {"error": "idempotency key reused with a different body"})  # => 409, rejected
        return stored_response  # => identical body -> safe replay, return the original
    new_id = NEXT_ID[0]  # => a fresh id for a genuinely new order
    order: dict[str, object] = {"id": new_id, "item": item}  # => the new resource
    STORE[new_id] = order  # => persists it
    response = Response(201, order)  # => the first-call response
    IDEMPOTENCY_STORE[idempotency_key] = (item, response)  # => co-06: record body + response together
    NEXT_ID[0] += 1  # => advances the counter
    return response  # => 201 on first create
 
 
first = create_order("key-X", "Widget")  # => genuine new order
print(f"first (Widget):    status={first.status}, body={first.body}")  # => Output: 201, id=1
 
safe_replay = create_order("key-X", "Widget")  # => SAME key, SAME body -> safe replay
print(f"replay (Widget):   status={safe_replay.status}, body={safe_replay.body}")  # => Output: 200, id=1
 
mismatch = create_order("key-X", "Gadget")  # => co-06: SAME key, DIFFERENT body -> REJECTED
print(f"mismatch (Gadget): status={mismatch.status}, body={mismatch.body}")  # => Output: 409, rejected
 
assert first.status == 201 and safe_replay.body == first.body  # => genuine + safe replay
assert mismatch.status == 409  # => co-06: a mismatched reuse is rejected, never applied
assert len(STORE) == 1  # => still only one order -- the mismatch created nothing

Run: python3 example.py

Output:

first (Widget):    status=201, body={'id': 1, 'item': 'Widget'}
replay (Widget):   status=201, body={'id': 1, 'item': 'Widget'}
mismatch (Gadget): status=409, body={'error': 'idempotency key reused with a different body'}

Key takeaway: A reused key with a different body is rejected -- it's a client bug, not a safe retry.

Why it matters: Rejecting on mismatch prevents a key collision from doing the wrong thing on either side.


Example 18: Repository -- a Collection-Like Interface

ex-18 · exercises co-09

A Repository mediates between the domain and the data store, exposing a collection-like interface (add/get/all/remove) so the domain never touches the storage mechanism (Fowler, PoEAA).

learning/code/ex-18-repository-crud/example.py

# pyright: strict
"""Example 18: Repository -- a collection-like interface over a store. (co-09)
 
A Repository mediates between the domain and the data store, exposing a
COLLECTION-LIKE interface (add/get/all/remove) so the domain never touches
the storage mechanism directly. Source: Martin Fowler, PoEAA -- "a
collection-like interface for accessing domain objects."
"""
 
from dataclasses import dataclass  # => a small typed domain object
 
 
@dataclass  # => the domain object this repository holds
class Task:
    id: int  # => the resource identifier
    title: str  # => the resource's own data
 
 
class TaskRepository:  # => co-09: a collection-like interface over an in-memory store
    def __init__(self) -> None:
        self._store: dict[int, Task] = {}  # => the private storage; callers never touch this
        self._next_id = 1  # => the id minted by the NEXT add()
 
    def add(self, title: str) -> Task:  # => add one member to the collection
        task = Task(id=self._next_id, title=title)  # => a new domain object
        self._store[task.id] = task  # => persists it
        self._next_id += 1  # => advances the id counter
        return task  # => returns the added member
 
    def get(self, task_id: int) -> Task | None:  # => fetch one member by id
        return self._store.get(task_id)  # => None when absent -- a collection's "not found"
 
    def all(self) -> list[Task]:  # => the whole collection
        return list(self._store.values())  # => a snapshot list, decoupled from internal storage
 
    def remove(self, task_id: int) -> bool:  # => remove one member
        if task_id in self._store:  # => present -> delete and report success
            del self._store[task_id]  # => removes the member
            return True  # => removed
        return False  # => nothing to remove
 
 
repo = TaskRepository()  # => co-09: the domain talks to THIS, not to a dict directly
created = repo.add("Write docs")  # => CREATE through the collection interface
repo.add("Ship release")  # => a second member
print(f"get(1): {repo.get(1)}")  # => Output: Task(id=1, title='Write docs')
print(f"all():  {repo.all()}")  # => Output: both tasks
print(f"remove(1): {repo.remove(1)}")  # => Output: True
print(f"get(1) after remove: {repo.get(1)}")  # => Output: None -- gone from the collection
 
assert repo.get(1) is None and len(repo.all()) == 1  # => co-09: full CRUD round-trip through the repository

Run: python3 example.py

Output:

get(1): Task(id=1, title='Write docs')
all():  [Task(id=1, title='Write docs'), Task(id=2, title='Ship release')]
remove(1): True
get(1) after remove: None

Key takeaway: The domain talks to a collection interface (add/get/all/remove), not a dict or a table.

Why it matters: Decoupling the domain from storage is what lets the backend be swapped without touching domain logic.


Example 19: Repository -- Swap the Backend, Callers Unchanged

ex-19 · exercises co-09

Two implementations (an in-memory dict and a 'sql-like' list-backed one) both satisfy the SAME protocol, so the domain code that uses them never changes when the backend is swapped.

learning/code/ex-19-repository-swap-backend/example.py

# pyright: strict
"""Example 19: Repository -- swap the backend, callers unchanged. (co-09)
 
A Repository is an ABSTRACTION over storage. Two implementations (an in-
memory dict and a "sql-like" list-backed one) both satisfy the SAME protocol,
so the domain code that uses them never changes when the backend is swapped.
"""
 
from dataclasses import dataclass  # => a small typed domain object
from typing import Protocol  # => Protocol: a structural interface both backends satisfy
 
 
@dataclass  # => the domain object
class Task:
    id: int  # => the resource identifier
    title: str  # => the resource's own data
 
 
class TaskRepository(Protocol):  # => co-09: the collection-like interface BOTH backends implement
    def add(self, title: str) -> Task: ...  # => add one member
    def all(self) -> list[Task]: ...  # => the whole collection
 
 
class InMemoryRepo:  # => co-09: backend 1 -- a dict
    def __init__(self) -> None:
        self._store: dict[int, Task] = {}  # => dict-backed storage
        self._next = 1  # => the next id
 
    def add(self, title: str) -> Task:
        task = Task(self._next, title)  # => builds the domain object
        self._store[task.id] = task  # => persists in a dict
        self._next += 1  # => advances the id
        return task  # => returns the added member
 
    def all(self) -> list[Task]:
        return list(self._store.values())  # => snapshot of the dict
 
 
class ListBackedRepo:  # => co-09: backend 2 -- a "sql-like" list of rows
    def __init__(self) -> None:
        self._rows: list[Task] = []  # => list-backed storage (stands in for a SQL table)
 
    def add(self, title: str) -> Task:
        next_id = (max((t.id for t in self._rows), default=0)) + 1  # => derives the next id from existing rows
        task = Task(next_id, title)  # => builds the domain object
        self._rows.append(task)  # => appends a row (stands in for an INSERT)
        return task  # => returns the added member
 
    def all(self) -> list[Task]:
        return list(self._rows)  # => snapshot of the list
 
 
def use(repo: TaskRepository) -> None:  # => co-09: domain code that depends on the PROTOCOL, not a backend
    repo.add("Shared task A")  # => add through the interface
    repo.add("Shared task B")  # => add through the interface
    print(f"  members: {[t.title for t in repo.all()]}")  # => Output: both tasks, regardless of backend
 
 
print("InMemoryRepo:")  # => Output: header for backend 1
use(InMemoryRepo())  # => co-09: callers unchanged
print("ListBackedRepo:")  # => Output: header for backend 2
use(ListBackedRepo())  # => co-09: SAME domain code, DIFFERENT backend
 
# The domain `use()` function references ONLY the protocol -- swapping the backend changed nothing in it.

Run: python3 example.py

Output:

InMemoryRepo:
  members: ['Shared task A', 'Shared task B']
ListBackedRepo:
  members: ['Shared task A', 'Shared task B']

Key takeaway: Same protocol, two backends -- the domain code referencing the protocol is unchanged by the swap.

Why it matters: This is the practical payoff of the repository abstraction: persistence is pluggable.


Example 20: Unit of Work -- Track Changes, Commit Once

ex-20 · exercises co-10

A Unit of Work maintains a list of objects affected by a business transaction and coordinates writing them out -- all changes commit together on commit() (Fowler, PoEAA).

learning/code/ex-20-unit-of-work-commit/example.py

# pyright: strict
"""Example 20: Unit of Work -- track changes, commit once. (co-10)
 
A Unit of Work maintains a list of objects affected by a business transaction
and coordinates writing them out -- all changes commit together on commit().
Source: Martin Fowler, PoEAA -- Unit of Work pattern.
"""
 
from dataclasses import dataclass  # => a small typed domain object
 
 
@dataclass  # => the domain object; mutated in memory, flushed by the UoW
class Task:
    id: int  # => the resource identifier
    title: str  # => mutated during the transaction
 
 
@dataclass  # => co-10: a fully-typed snapshot of everything commit() writes out, together
class Flush:
    inserted: list[dict[str, object]]  # => the new objects, serialized
    updated: list[dict[str, object]]  # => the modified objects, serialized
    deleted_ids: list[int]  # => the removed ids
 
 
class UnitOfWork:  # => co-10: tracks the objects changed in one business transaction
    def __init__(self) -> None:
        self._new: list[Task] = []  # => objects to INSERT on commit
        self._dirty: list[Task] = []  # => objects to UPDATE on commit
        self._deleted_ids: list[int] = []  # => object ids to DELETE on commit
        self.committed = False  # => whether commit() has run
 
    def register_new(self, task: Task) -> None:  # => track a brand-new object
        self._new.append(task)  # => queued for INSERT
 
    def register_dirty(self, task: Task) -> None:  # => track a modified existing object
        self._dirty.append(task)  # => queued for UPDATE
 
    def register_deleted(self, task_id: int) -> None:  # => track a removal
        self._deleted_ids.append(task_id)  # => queued for DELETE
 
    def commit(self) -> Flush:  # => co-10: write ALL tracked changes out, together
        flush = Flush(  # => one coordinated write-out of every change
            inserted=[t.__dict__ for t in self._new],  # => the new objects, serialized
            updated=[t.__dict__ for t in self._dirty],  # => the modified objects, serialized
            deleted_ids=list(self._deleted_ids),  # => the removed ids
        )
        self.committed = True  # => the transaction is now durably written
        return flush  # => the single, coordinated result
 
 
uow = UnitOfWork()  # => co-10: open a business transaction
uow.register_new(Task(id=1, title="New task"))  # => stage an INSERT (not written yet)
existing = Task(id=2, title="old")  # => an existing object loaded into the transaction
existing.title = "updated"  # => mutate it in memory
uow.register_dirty(existing)  # => stage an UPDATE (not written yet)
uow.register_deleted(7)  # => stage a DELETE (not written yet)
print(f"committed before commit(): {uow.committed}")  # => Output: False -- nothing written yet
 
flush = uow.commit()  # => co-10: ALL three changes land TOGETHER in one write-out
print(f"flush: {flush}")  # => Output: inserted + updated + deleted, all at once
 
assert uow.committed is True  # => co-10: the transaction committed
assert len(flush.inserted) == 1 and len(flush.updated) == 1 and flush.deleted_ids == [7]  # => all three changes landed

Run: python3 example.py

Output:

committed before commit(): False
flush: Flush(inserted=[{'id': 1, 'title': 'New task'}], updated=[{'id': 2, 'title': 'updated'}], deleted_ids=[7])

Key takeaway: A UoW stages every change and writes them ALL out together on commit(), not piecemeal.

Why it matters: Staging changes is the precondition for the atomic-commit boundary (co-11).


Example 21: Unit of Work -- Rollback Discards Tracked Changes

ex-21 · exercises co-10, co-11

Rolling back a Unit of Work throws away EVERY staged change: nothing the transaction registered is written out. This is the atomic-commit boundary from the discard side.

learning/code/ex-21-unit-of-work-rollback/example.py

# pyright: strict
"""Example 21: Unit of Work -- rollback discards tracked changes. (co-10, co-11)
 
Rolling back a Unit of Work throws away EVERY staged change: nothing the
transaction registered is written out. This is the atomic-commit boundary
(co-11) from the discard side -- either everything commits, or nothing does.
"""
 
from dataclasses import dataclass  # => a small typed domain object
 
 
@dataclass  # => the domain object staged in the transaction
class Task:
    id: int  # => the resource identifier
    title: str  # => the resource's own data
 
 
STORE: dict[int, Task] = {1: Task(1, "persistent")}  # => the durable store -- rollback must leave it untouched
 
 
class UnitOfWork:  # => co-10: a staging area for one business transaction
    def __init__(self, store: dict[int, Task]) -> None:
        self._store = store  # => the durable backing store
        self._pending: list[Task] = []  # => staged changes, NOT yet written
 
    def register_new(self, task: Task) -> None:  # => stage an insert
        self._pending.append(task)  # => queued, not yet visible in the store
 
    def commit(self) -> None:  # => co-10: write every staged change into the store
        for task in self._pending:  # => flush each staged object
            self._store[task.id] = task  # => durably written
 
    def rollback(self) -> None:  # => co-10/co-11: discard ALL staged changes
        self._pending.clear()  # => nothing staged is ever written -- the store is untouched
 
 
uow = UnitOfWork(STORE)  # => open a transaction over the durable store
uow.register_new(Task(2, "ephemeral"))  # => stage a change -- NOT yet in STORE
print(f"store before rollback: { {k: v.title for k, v in STORE.items()} }")  # => Output: only id 1
 
uow.rollback()  # => co-10/co-11: discard -- the staged task is gone
print(f"store after rollback:  { {k: v.title for k, v in STORE.items()} }")  # => Output: still only id 1
 
assert 2 not in STORE and STORE[1].title == "persistent"  # => co-11: no tracked change persisted after rollback

Run: python3 example.py

Output:

store before rollback: {1: 'persistent'}
store after rollback:  {1: 'persistent'}

Key takeaway: Rollback discards every staged change -- the durable store is untouched.

Why it matters: Either everything commits or nothing does -- the ACID atomicity guarantee.


Example 22: Transaction Atomicity -- All Commit or All Roll Back

ex-22 · exercises co-11

Two writes inside one transaction: when both succeed they commit together; when a failure occurs MID-transaction, both roll back and neither survives.

learning/code/ex-22-transaction-atomic/example.py

# pyright: strict
"""Example 22: Transaction Atomicity -- all commit or all roll back. (co-11)
 
Two writes inside one transaction: when both succeed they commit together;
when a failure occurs MID-transaction, both roll back and neither write
survives. This is the ACID atomicity guarantee -- an all-or-nothing commit
boundary.
"""
 
from dataclasses import dataclass, field  # => field: mutable-default-safe factories
 
 
@dataclass  # => a typed account -- avoids dict[str, str|int] inference problems
class Account:
    name: str  # => the account label (for readable write descriptions)
    balance: int  # => the balance mutated inside the transaction
 
 
@dataclass  # => co-11: a transaction collects writes and commits or rolls them back atomically
class Transaction:
    writes: list[str] = field(default_factory=list[str])  # => staged writes, in order
    committed: bool = False  # => whether the transaction reached commit
 
 
def transfer(from_account: Account, to_account: Account, amount: int) -> Transaction:
    # => co-11: two writes (debit + credit) that must be ATOMIC
    tx = Transaction()  # => open the transaction
    tx.writes.append(f"debit {from_account.name} {amount}")  # => stage write 1: debit
    if from_account.balance < amount:  # => a mid-transaction FAILURE (insufficient funds)
        return tx  # => co-11: rollback -- neither write is committed (committed stays False)
    from_account.balance -= amount  # => apply write 1
    tx.writes.append(f"credit {to_account.name} {amount}")  # => stage write 2: credit
    to_account.balance += amount  # => apply write 2
    tx.committed = True  # => co-11: both writes succeeded -> commit atomically
    return tx  # => the committed transaction
 
 
# Case A: enough funds -> both writes commit together.
alice = Account("alice", 100)  # => account 1
bob = Account("bob", 50)  # => account 2
ok = transfer(alice, bob, 30)  # => co-11: both writes land
print(f"success: committed={ok.committed}, alice={alice.balance}, bob={bob.balance}")  # => Output: True, 70, 80
print(f"  writes: {ok.writes}")  # => Output: both debit and credit
 
# Case B: insufficient funds -> failure mid-transaction -> both roll back.
carol = Account("carol", 10)  # => account with too little
dave = Account("dave", 0)  # => account 2
failed = transfer(carol, dave, 500)  # => co-11: fails after staging the debit -> rollback
print(f"failure: committed={failed.committed}, carol={carol.balance}, dave={dave.balance}")  # => Output: False, 10, 0
print(f"  writes: {failed.writes}")  # => Output: only the staged debit (never applied/committed)
 
assert ok.committed and alice.balance == 70 and bob.balance == 80  # => co-11: success committed both
assert not failed.committed and carol.balance == 10 and dave.balance == 0  # => co-11: failure rolled both back

Run: python3 example.py

Output:

success: committed=True, alice=70, bob=80
  writes: ['debit alice 30', 'credit bob 30']
failure: committed=False, carol=10, dave=0
  writes: ['debit carol 500']

Key takeaway: A debit and a credit commit together, or both roll back -- never half-applied.

Why it matters: Atomicity is what makes a multi-step business operation all-or-nothing instead of leaving the books unbalanced.


Example 23: JWT -- Encode, Sign, and Verify

ex-23 · exercises co-14

A JWT is a signed, self-contained claims token (RFC 7519): header.payload.signature, base64url-encoded, signed with HMAC-SHA256. The receiver verifies the signature WITHOUT a session lookup.

learning/code/ex-23-jwt-encode-decode/example.py

# pyright: strict
"""Example 23: JWT -- encode, sign, and verify (HS256, hand-rolled). (co-14)
 
A JWT is a signed, self-contained claims token (RFC 7519): header.payload.
signature, base64url-encoded, signed with HMAC-SHA256. The receiver verifies
the signature WITHOUT a session lookup -- the token itself carries the claims.
"""
 
import base64  # => stdlib: base64url encoding for each JWT segment
import hashlib  # => stdlib: SHA-256 for the HMAC
import hmac  # => stdlib: HMAC-SHA256 signing + constant-time compare
import json  # => stdlib: serialize the header and claims to compact JSON
 
SECRET = b"super-secret-key"  # => the HMAC shared secret (in production, loaded from env, never hardcoded in source shipped to a client)
 
 
def b64url(raw: bytes) -> str:  # => base64url-encode WITHOUT "=" padding (the JWT segment format)
    return base64.urlsafe_b64encode(raw).rstrip(b"=").decode("ascii")  # => strips padding per RFC 7515
 
 
def sign(message: bytes) -> str:  # => HMAC-SHA256, then base64url -- the JWT signature segment
    return b64url(hmac.new(SECRET, message, hashlib.sha256).digest())  # => keyed hash over header.payload
 
 
def encode(claims: dict[str, object]) -> str:  # => co-14: build a signed compact JWT
    header = b64url(json.dumps({"alg": "HS256", "typ": "JWT"}, separators=(",", ":")).encode())  # => segment 1: header
    payload = b64url(json.dumps(claims, separators=(",", ":")).encode())  # => segment 2: claims
    signing_input = f"{header}.{payload}".encode()  # => the bytes the signature covers
    signature = sign(signing_input)  # => segment 3: signature
    return f"{header}.{payload}.{signature}"  # => the compact serialised JWT
 
 
def verify(token: str) -> dict[str, object] | None:  # => co-14: verify signature + return claims, or None on tamper
    try:
        header_b64, payload_b64, signature = token.split(".")  # => the three segments
    except ValueError:  # => not three segments -> malformed
        return None  # => reject
    expected = sign(f"{header_b64}.{payload_b64}".encode())  # => recompute the signature over the same input
    if not hmac.compare_digest(expected, signature):  # => co-14: constant-time compare; mismatch -> tampered
        return None  # => reject
    raw = base64.urlsafe_b64decode(payload_b64 + "==")  # => re-pad before decoding the payload
    return json.loads(raw)  # => the verified claims
 
 
token = encode({"sub": "user-42", "role": "admin"})  # => co-14: issue a signed token
print(f"token: {token[:48]}...")  # => Output: a prefix of the compact JWT (truncated for display)
 
claims = verify(token)  # => co-14: verify + decode -- no session lookup needed
print(f"verified claims: {claims}")  # => Output: {'sub': 'user-42', 'role': 'admin'}
 
tampered = token[:-2] + ("AA" if not token.endswith("AA") else "BB")  # => corrupt the signature segment
print(f"tampered verifies: {verify(tampered)}")  # => Output: None -- signature mismatch rejected
 
assert claims == {"sub": "user-42", "role": "admin"}  # => co-14: round-trip succeeds
assert verify(tampered) is None  # => co-14: a tampered token is rejected

Run: python3 example.py

Output:

token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ...
verified claims: {'sub': 'user-42', 'role': 'admin'}
tampered verifies: None

Key takeaway: A JWT is verified by recomputing the HMAC over header.payload -- no session store needed.

Why it matters: A tampered token is rejected because its signature no longer matches -- stateless verification.


Example 24: JWT -- an Expired Token Is Rejected

ex-24 · exercises co-14

A JWT carries an exp (expiry) claim: the receiver rejects any token whose exp is in the past, limiting a stolen token's lifetime.

learning/code/ex-24-jwt-expiry/example.py

# pyright: strict
"""Example 24: JWT -- an expired token is rejected. (co-14)
 
A JWT carries an `exp` (expiry) claim: the receiver rejects any token whose
exp is in the past. This is how short-lived tokens limit the blast radius of a
stolen token. Source: RFC 7519 Sec 4.1.4 (`exp`).
"""
 
import base64  # => stdlib: base64url segment encoding
import hashlib  # => stdlib: SHA-256 for the HMAC
import hmac  # => stdlib: HMAC-SHA256 signing
import json  # => stdlib: serialize header and claims
 
 
SECRET = b"super-secret-key"  # => the HMAC shared secret
 
 
def b64url(raw: bytes) -> str:  # => base64url without padding
    return base64.urlsafe_b64encode(raw).rstrip(b"=").decode("ascii")  # => JWT segment format
 
 
def sign(message: bytes) -> str:  # => HMAC-SHA256 -> base64url
    return b64url(hmac.new(SECRET, message, hashlib.sha256).digest())  # => keyed hash
 
 
def encode(claims: dict[str, object]) -> str:  # => build a signed compact JWT
    header = b64url(json.dumps({"alg": "HS256", "typ": "JWT"}, separators=(",", ":")).encode())  # => header segment
    payload = b64url(json.dumps(claims, separators=(",", ":")).encode())  # => claims segment
    return f"{header}.{payload}.{sign(f'{header}.{payload}'.encode())}"  # => compact JWT
 
 
def verify(token: str, now: int) -> dict[str, object] | None:  # => co-14: signature + expiry check
    header_b64, payload_b64, signature = token.split(".")  # => the three segments
    if not hmac.compare_digest(sign(f"{header_b64}.{payload_b64}".encode()), signature):  # => signature mismatch
        return None  # => reject
    claims: dict[str, object] = json.loads(base64.urlsafe_b64decode(payload_b64 + "=="))  # => decode claims
    exp = claims.get("exp")  # => co-14: the expiry timestamp
    if isinstance(exp, int) and now >= exp:  # => co-14: `now` is AT/AFTER exp -> token expired
        return None  # => reject -- the token's lifetime is over
    return claims  # => fresh enough -> valid
 
 
fresh = encode({"sub": "user-1", "exp": 2000})  # => a token expiring at t=2000
expired = encode({"sub": "user-1", "exp": 1000})  # => a token that already expired at t=1000
 
print(f"fresh at now=1500:   {verify(fresh, now=1500)}")  # => Output: claims -- now < exp
print(f"expired at now=1500: {verify(expired, now=1500)}")  # => Output: None -- now >= exp
 
assert verify(fresh, now=1500) is not None  # => co-14: a not-yet-expired token passes
assert verify(expired, now=1500) is None  # => co-14: an expired token is rejected

Run: python3 example.py

Output:

fresh at now=1500:   {'sub': 'user-1', 'exp': 2000}
expired at now=1500: None

Key takeaway: An expired token (now >= exp) is rejected, even though its signature is still valid.

Why it matters: Short-lived tokens cap the blast radius of a stolen credential.


Example 25: RBAC -- a Role-Restricted Route

ex-25 · exercises co-17

Role-Based Access Control maps users -> roles -> privileges (the NIST model). A route demands a role; the right role is allowed (200), the wrong role is denied (403).

learning/code/ex-25-rbac-role-gate/example.py

# pyright: strict
"""Example 25: RBAC -- a role-restricted route. (co-17)
 
Role-Based Access Control (RBAC) maps users -> roles -> privileges (the NIST
model, Ferraiolo & Kuhn 1992, INCITS 359). A route demands a role; a user
with the right role is allowed (200), a user with the wrong role is denied
(403). Source: NIST CSRC RBAC.
"""
 
from dataclasses import dataclass  # => a small typed response record
 
 
@dataclass  # => co-17: status plus a short message
class Response:
    status: int  # => 200 allowed, 403 forbidden
    body: dict[str, str]  # => a short message
 
 
# users -> role; the role -> the set of privileges it grants.
USERS: dict[str, str] = {"ada": "admin", "grace": "viewer"}  # => the user -> role mapping
ROLE_PRIVILEGES: dict[str, set[str]] = {"admin": {"billing:read", "billing:write"}, "viewer": {"billing:read"}}  # => role -> privileges
 
 
def view_billing(username: str) -> Response:  # => GET /billing -- requires billing:read
    role = USERS.get(username)  # => the user's role (None if unknown)
    privileges = ROLE_PRIVILEGES.get(role or "", set())  # => the privileges that role grants
    if "billing:read" not in privileges:  # => co-17: the role lacks the required privilege
        return Response(403, {"error": f"role {role!r} cannot read billing"})  # => 403
    return Response(200, {"billing": "ok", "viewed_by": username})  # => 200, allowed
 
 
admin = view_billing("ada")  # => role "admin" has billing:read
print(f"admin views billing:  status={admin.status}, body={admin.body}")  # => Output: 200
 
viewer = view_billing("grace")  # => role "viewer" ALSO has billing:read
print(f"viewer views billing: status={viewer.status}, body={viewer.body}")  # => Output: 200
 
# A write attempt needs billing:write, which "viewer" lacks -- the RBAC gate denies it.
def write_billing(username: str) -> Response:  # => POST /billing -- requires billing:write
    role = USERS.get(username)  # => the user's role
    privileges = ROLE_PRIVILEGES.get(role or "", set())  # => that role's privileges
    if "billing:write" not in privileges:  # => co-17: lacks the write privilege
        return Response(403, {"error": f"role {role!r} cannot write billing"})  # => 403
    return Response(200, {"written_by": username})  # => 200
 
 
viewer_write = write_billing("grace")  # => "viewer" lacks billing:write -> 403
print(f"viewer writes billing: status={viewer_write.status}, body={viewer_write.body}")  # => Output: 403
 
assert admin.status == 200 and viewer.status == 200  # => co-17: read allowed for both roles
assert write_billing("ada").status == 200 and viewer_write.status == 403  # => co-17: write gated by role

Run: python3 example.py

Output:

admin views billing:  status=200, body={'billing': 'ok', 'viewed_by': 'ada'}
viewer views billing: status=200, body={'billing': 'ok', 'viewed_by': 'grace'}
viewer writes billing: status=403, body={'error': "role 'viewer' cannot write billing"}

Key takeaway: RBAC gates an operation by role -- the right role gets 200, the wrong role gets 403.

Why it matters: RBAC is auditable and coarse; ABAC (next) is flexible but harder to manage -- a real trade-off.


Example 26: ABAC -- an Owner-Only Attribute Policy

ex-26 · exercises co-17

Attribute-Based Access Control decides access from RULES over ATTRIBUTES (the requester's, the resource's). Here the policy is 'the requester must OWN the resource'.

learning/code/ex-26-abac-attribute-gate/example.py

# pyright: strict
"""Example 26: ABAC -- an owner-only attribute policy. (co-17)
 
Attribute-Based Access Control (ABAC) decides access from RULES over
ATTRIBUTES (the requester's, the resource's, the environment's) rather than
a fixed role table. Here the policy is "the requester must OWN the resource"
-- flexible, but the rule set is harder to audit than RBAC. Source: NIST CSRC.
"""
 
from dataclasses import dataclass  # => a small typed domain + response record
 
 
@dataclass  # => the resource carries its OWN owner attribute -- the thing the policy reads
class Document:
    id: int  # => the resource identifier
    owner: str  # => co-17: an ATTRIBUTE of the resource -- who owns it
 
 
@dataclass  # => co-17: status plus a short message
class Response:
    status: int  # => 200 allowed, 403 denied
    body: dict[str, str]  # => a short message
 
 
DOCS: dict[int, Document] = {1: Document(1, "ada"), 2: Document(2, "grace")}  # => resources with owner attributes
REQUESTERS: dict[str, str] = {"ada-token": "ada", "grace-token": "grace"}  # => token -> requester identity attribute
 
 
def view_doc(token: str, doc_id: int) -> Response:  # => GET /docs/{id} -- the ABAC owner policy
    requester = REQUESTERS.get(token)  # => the requester's identity ATTRIBUTE
    if requester is None:  # => no identity attribute at all
        return Response(401, {"error": "unknown requester"})  # => 401
    doc = DOCS.get(doc_id)  # => the resource (with its owner attribute)
    if doc is None:  # => resource does not exist
        return Response(404, {"error": "not found"})  # => 404
    # co-17: the ABAC RULE -- requester attribute (identity) must EQUAL resource attribute (owner)
    if requester != doc.owner:  # => the rule evaluates to DENY -- a non-owner is rejected
        return Response(403, {"error": "only the owner may view"})  # => 403
    return Response(200, {"doc_id": str(doc_id), "viewed_by": requester})  # => 200, owner
 
 
owner = view_doc("ada-token", 1)  # => ada owns doc 1 -> allowed
print(f"owner views own doc:    status={owner.status}, body={owner.body}")  # => Output: 200
 
non_owner = view_doc("grace-token", 1)  # => grace does NOT own doc 1 -> denied by the attribute rule
print(f"non-owner views doc 1:  status={non_owner.status}, body={non_owner.body}")  # => Output: 403
 
assert owner.status == 200  # => co-17: the owner attribute matches -> allowed
assert non_owner.status == 403  # => co-17: a non-owner is denied by the rule (ABAC, not a role table)

Run: python3 example.py

Output:

owner views own doc:    status=200, body={'doc_id': '1', 'viewed_by': 'ada'}
non-owner views doc 1:  status=403, body={'error': 'only the owner may view'}

Key takeaway: ABAC reads attributes (identity vs owner) and applies a rule -- flexible, but the rule set is harder to audit than a role table.

Why it matters: RBAC vs ABAC is a real trade-off: auditable-and-coarse vs flexible-and-complex.


Example 27: Structured Logging -- a JSON Line with Typed Fields

ex-27 · exercises co-25

A structured log is a machine-parseable JSON object with typed fields (timestamp, level, message, request_id) rather than free text.

learning/code/ex-27-structured-log-json/example.py

# pyright: strict
"""Example 27: Structured Logging -- a JSON line with typed fields. (co-25)
 
A structured log is a machine-parseable JSON object with typed fields
(timestamp, level, message, request_id) rather than free text. This example
emits one such line and verifies it parses back to a dict with the expected
keys -- so a downstream log aggregator can index every field.
"""
 
import json  # => stdlib: serialize the record to a JSON line and parse it back
 
 
def log_json(level: str, message: str, request_id: str, timestamp: str = "2026-07-29T10:00:00Z") -> str:
    # => co-25: every field is a TYPED value, not interpolated prose
    record: dict[str, str] = {  # => the structured fields
        "timestamp": timestamp,  # => when the event happened (ISO-8601)
        "level": level,  # => severity: info/warn/error
        "message": message,  # => a short human-readable summary
        "request_id": request_id,  # => co-25: the correlation key tying this line to one request
    }
    return json.dumps(record)  # => one JSON line, ready for a log shipper to ingest
 
 
line = log_json(level="info", message="order created", request_id="req-abc-123")  # => emit one structured line
print(line)  # => Output: a single JSON object on one line
 
parsed = json.loads(line)  # => co-25: verify it round-trips into a parseable dict
print(f"parsed message: {parsed['message']}")  # => Output: order created
 
expected_keys = {"timestamp", "level", "message", "request_id"}  # => the typed fields every line carries
assert set(parsed.keys()) == expected_keys  # => co-25: the line parses with exactly these fields
assert parsed["level"] == "info" and parsed["request_id"] == "req-abc-123"  # => fields are typed values

Run: python3 example.py

Output:

{"timestamp": "2026-07-29T10:00:00Z", "level": "info", "message": "order created", "request_id": "req-abc-123"}
parsed message: order created

Key takeaway: A structured log line is a JSON object with typed fields, so an aggregator can index every one.

Why it matters: Free-text logs are unparseable downstream; structured logs are queryable.


Example 28: Correlation ID -- Thread One Id Through Every Log Line

ex-28 · exercises co-25

A correlation (request) id is minted at the request boundary and carried on EVERY log line the request produces, so all of a request's lines group together across functions and services.

learning/code/ex-28-correlation-id/example.py

# pyright: strict
"""Example 28: Correlation ID -- thread one id through every log line. (co-25)
 
A correlation (request) id is minted at the request boundary and carried on
EVERY log line the request produces, so all of a request's lines group
together in a log aggregator -- across functions, across services.
"""
 
import json  # => stdlib: each log line is a JSON object
 
 
LOG_BUFFER: list[str] = []  # => collects the request's lines, so we can inspect them together
 
 
def log(level: str, message: str, request_id: str) -> None:  # => co-25: every line carries the SAME request_id
    LOG_BUFFER.append(json.dumps({"level": level, "message": message, "request_id": request_id}))  # => stamped with the id
 
 
def handle_request(request_id: str) -> None:  # => a request's lifetime, all lines sharing the id
    log("info", "received request", request_id)  # => entry line, stamped
    validate(request_id)  # => a deeper function still threads the SAME id
    log("info", "request complete", request_id)  # => exit line, stamped
 
 
def validate(request_id: str) -> None:  # => a nested call -- still carries the id down
    log("debug", "validating payload", request_id)  # => co-25: the id propagates across function boundaries
 
 
handle_request("req-xyz-789")  # => one request, three lines, one shared id
for line in LOG_BUFFER:  # => print every line the request produced
    print(line)  # => Output: three JSON lines, all carrying request_id=req-xyz-789
 
parsed = [json.loads(line) for line in LOG_BUFFER]  # => parse them all back
all_carry_id = all(entry["request_id"] == "req-xyz-789" for entry in parsed)  # => co-25: every line carries the id
print(f"every line carries the correlation id: {all_carry_id}")  # => Output: True
assert all_carry_id and len(LOG_BUFFER) == 3  # => co-25: three lines, one shared id throughout

Run: python3 example.py

Output:

{"level": "info", "message": "received request", "request_id": "req-xyz-789"}
{"level": "debug", "message": "validating payload", "request_id": "req-xyz-789"}
{"level": "info", "message": "request complete", "request_id": "req-xyz-789"}
every line carries the correlation id: True

Key takeaway: One id on every line groups a whole request's logs together in an aggregator.

Why it matters: Without a correlation id, a request's scattered log lines are impossible to reassemble.


← Previous: Overview · Next: Intermediate Examples

Last updated July 28, 2026

Command Palette

Search for a command to run...