Rustic Python
Make type-checkable Python mistakes fail static checks before execution.
A misspelled record field, an unhandled failure, or a method called before setup can become a type-checking error. Logic and numerical correctness still need tests.
Each page covers one concept: a situation, typical Python code, a better alternative, and why it helps. Examples and their results are visible on the page. No cloning, Python installation, or terminal is needed.
The book assumes familiarity with Python functions, classes, and basic annotations. It uses Python 3.11+ and requires no Rust or machine-learning background.
Start reading
Begin with the goal and example notation. Result and match shows how a recoverable error becomes a checked outcome. Total operations applies that idea to an expected missing lookup. Then follow validated records, variants, and exhaustive matching.
Later pages cover state, protocols, external boundaries, and checker limitations.
Optional references cover agent setup, contributing, and the changelog.
Licensed under the MIT license.
Move type-checkable mistakes into static checks
Project overview and reading path
The purpose of this guide is to make type-checkable mistakes fail static checks instead of surfacing only when Python runs. Every example is one view of that goal: a misspelled field, an unhandled failure, an invalid state transition, or an incompatible plugin.
For example, metadata["worker_count"] can raise KeyError after a job starts.
If the record declares a workers field, the checker rejects
metadata.worker_count before the job starts. The record lesson
shows both programs and the rejected operation.
External data can still be malformed at runtime; a checker cannot inspect JSON
that has not arrived yet. Validate it at the boundary. When the caller can reject,
correct, or retry it, return a typed success-or-failure outcome. The guide borrows
Result<T, E> from Rust’s standard library: Ok carries a value and Err
carries an expected error.
Python has no built-in Result, but a small Ok[T] | Err[E] union makes the same
choice visible to the checker. A caller can use match to handle both cases;
assert_never checks that its branches cover the union. See the Result and
match example before the other data lessons.
That is why the guide is called Rustic Python: it borrows explicit outcomes,
closed variants, and state-dependent types where they make misuse checkable. It
does not promise Rust’s ownership rules or exception-free execution. Merely
replacing a late KeyError with a late ValueError would miss the point.
Static checks do not establish that an algorithm is logically correct. They also cannot prove numerical correctness, tensor shapes, or that Python will never raise. Each lesson identifies the exact mistake made checkable and names the runtime obligations that remain.
How to read the examples
Project overview and reading path
Each recommendation follows one situation: typical Python, a better alternative, and what changes for the caller. Short sketches omit imports and unrelated setup. Each Source link opens the complete checked example within the book; you do not need Python, a terminal, or a repository clone.
A line such as # rejected[missing-attribute]: workers = outcome.workers
means the checker rejects the statement after the colon. Here the model declares
workers, but the outcome might be InvalidMetadata. The diagnostic category is
shown so the result is visible without running anything. These comments are not
suppressions.
The examples use Python 3.11+, Pyrefly 1.3.1, and Ruff 0.16.9. Pydantic validates external inputs; pytest checks behavior. These are the book’s reference tools, not reader prerequisites. Contributor checks verify source links, runtime behavior, and the intended diagnostic for each rejected statement.
Result and match
Project overview and reading path
An importer reads integer labels. A malformed row should be reported while valid rows continue. The caller needs both the parsed value and a way to recognize rejection.
Typical Python
def parse_label(raw: str) -> int:
return int(raw)
labels = [parse_label(raw) for raw in ("7", "cat", "2")]
The annotation exposes only success. "cat" raises ValueError at runtime and interrupts the import. Returning -1 instead would still look like a valid integer to the checker.
Alternative
Result: TypeAlias = Ok[T] | Err[E]
def parse_label(raw: str) -> Result[int, InvalidLabel]:
try:
return Ok(LABEL.validate_python(raw))
except ValidationError:
return Err(InvalidLabel(raw, "not a nonnegative integer"))
outcome = parse_label("cat")
match outcome:
case Ok(value=label):
description = f"class {label}"
case Err(error=error):
description = f"rejected {error.raw!r}: {error.reason}"
case _:
assert_never(outcome)
parse_label("7") returns Ok(7); parse_label("cat") returns an
Err carrying the rejected input and reason. describe_label uses match to
consume either outcome. It produces "class 7" or a rejection message.
Result and match work together: the union exposes failure, matching narrows
the payload, and assert_never checks that no variant is forgotten. Assigning
the result directly to int, or reading .value before narrowing, is rejected.
A Raises: docstring cannot provide that checked contract.
Rust’s standard library provides Result<T, E> as an enum. Python does not
provide the equivalent, so this example uses an ordinary union of two small
frozen dataclasses, without inheritance or a third-party package. Ok and Err
give a reusable shape when an operation has one success type and an expected
error. Some later lessons use a direct union such as
JobMetadata | InvalidMetadata: naming the domain variants directly keeps
those examples focused. Both designs require narrowing before the success
value is used; match plus assert_never checks coverage of either closed union.
Use Result for supported retry, fallback, correction, or record rejection.
Use exceptions when the operation has no recovery path and must unwind. Translate
specific recoverable library exceptions at the boundary; do not disguise bugs.
Deliberately raising for a broken internal invariant is analogous to Rust’s
panic!: the operation cannot produce a valid result under that condition.
Python can still raise unexpected exceptions, and a caller can discard the whole
result. No return annotation proves exception freedom.
The parser accepts nonnegative integer-valued text, including "7.0"; it does
not know the dataset’s class count. The two frozen variants freeze their own
fields, not mutable payloads. Keep this small union rather than adding a result
framework or unchecked unwrap methods.
Prefer total operations for expected failures
Project overview and reading path
A user lookup may receive an ID that is absent from the current mapping. The caller can report the missing ID and continue, so absence is part of the valid input domain. An operation that has a defined outcome for every valid input is total; an operation that can fail for a valid input is partial.
Typical Python
def find_user(users: Mapping[str, User], user_id: str) -> User:
return users[user_id]
users = {"u-7": User("Ada")}
user = find_user(users, "u-9")
If users contains only "u-7", this call raises KeyError at runtime. Its
return annotation does not tell the caller that a missing ID needs handling.
Alternative
def find_user(users: Mapping[str, User], user_id: str) -> Result[User, UserNotFound]:
user = users.get(user_id)
if user is None:
return Err(UserNotFound(user_id))
return Ok(user)
users = {"u-7": User("Ada")}
outcome = find_user(users, "u-9")
match outcome:
case Ok(value=user):
message = user.name
case Err(error=UserNotFound(user_id=missing_id)):
message = f"user {missing_id} not found"
case _:
assert_never(outcome)
find_user(users, "u-7") returns Ok(User(name="Ada"));
find_user(users, "u-9") returns Err(UserNotFound(user_id="u-9")).
Matching the result produces "Ada" or "user u-9 not found". The checker
rejects found.value.name before the caller narrows Ok | Err, because Err
has no value attribute. That is the static improvement: the expected missing
key is visible in the return type. The checker does not reject every use of
users[user_id] or prove that arbitrary code cannot raise.
Use the same question for items[index], next(iterator), min(items), and
division: can an out-of-range index, exhausted iterator, empty collection, or
zero denominator occur during normal use? If so, choose an explicit outcome
that lets the caller make its real decision. Prefer the small Result union
for expected failures the caller can handle; do not build a generic wrapper
around every built-in.
When failure means a broken internal invariant and there is no supported
recovery, let the operation raise. See Result and match
for that distinction.
Validated records
Project overview and reading path
A worker receives job metadata. Every job needs a name and a positive worker count; downstream code must use the declared field names.
Typical Python
metadata: dict[str, str | int] = {"name": "report", "workers": 4}
workers = metadata["worker_count"]
The dictionary annotation allows any string key. This typo survives checking and
raises KeyError when the field is read.
Alternative
class JobMetadata(BaseModel):
name: str
workers: Annotated[int, Field(ge=1)]
def parse_metadata(payload: str) -> JobMetadata | InvalidMetadata:
try:
return JobMetadata.model_validate_json(payload)
except ValidationError as error:
return InvalidMetadata(str(error))
outcome = parse_metadata('{"name": "report", "workers": 4}')
match outcome:
case JobMetadata(workers=workers):
summary = f"{workers} workers"
case InvalidMetadata(reason=reason):
summary = f"rejected: {reason}"
case _:
assert_never(outcome)
The typed rejection follows the Result lesson; this page focuses on the named record fields.
For this input, parsing returns JobMetadata(name="report", workers=4). The
checker rejects both a misspelled model field and direct access to .workers
on the unhandled MetadataResult. Matching narrows the successful branch.
Pydantic validates external JSON at runtime. Nonpositive, boolean, string, and
float worker counts, missing fields, and malformed JSON return InvalidMetadata;
extra fields are ignored by this schema. The checker then requires a caller to
distinguish a valid record from that rejection before using its fields.
Direct model construction validates too; model_construct and
model_copy(update=...) can bypass checks. Validate the actual admission path.
Variants instead of optional fields
Project overview and reading path
A service supports classification and regression. Classification needs a class count; regression needs a positive error threshold. A request must select exactly one.
Typical Python
from dataclasses import dataclass
@dataclass
class Task:
kind: str
num_classes: int | None = None
huber_delta: float | None = None
task = Task(kind="classification", huber_delta=0.5)
if task.kind == "classification":
if task.num_classes is None:
raise ValueError("class count required")
num_outputs = task.num_classes + 1
This well-typed object lacks the classifier setting and contains an unrelated
regression setting. The caller must check the missing field, and this example
raises ValueError only when it reaches that guard at runtime.
Alternative
class Classification(BaseModel):
kind: Literal["classification"] = "classification"
num_classes: int
class Regression(BaseModel):
kind: Literal["regression"] = "regression"
huber_delta: float
Task = Classification | Regression
classification = Classification(num_classes=10)
num_outputs = classification.num_classes + 1
Each variant owns its required fields. The complete example adds boundary validation and a typed rejection, using the Result idea.
Classification(huber_delta=0.5) is rejected: num_classes is missing and
huber_delta is unexpected. Classification(num_classes=10) is valid.
Each variant owns only its relevant fields.
For JSON input, the kind tag selects the runtime schema. Missing or unknown
tags and mismatched fields return InvalidTask. The checker rejects passing the
unhandled TaskResult to loss_name; matching the valid variants narrows it.
The checker cannot establish positive numbers or validate JSON itself.
@final prevents checked subclassing, not runtime class manipulation.
Use a literal or enum when an alternative needs no associated fields.
Exhaustive matching
Project overview and reading path
A data pipeline decides which splits may fit a preprocessor. Later, someone adds a holdout split. Existing decisions should be reviewed when the set changes.
Typical Python
def may_fit_preprocessor(split: str) -> bool:
match split:
case "train":
return True
case _:
return False
allowed = may_fit_preprocessor("holdout")
The new split silently falls into the default. Nothing tells the author that an existing decision now covers an unreviewed case.
Alternative
Split = Literal["train", "validation", "test"]
def may_fit_preprocessor(split: Split) -> bool:
match split:
case "train":
return True
case "validation" | "test":
return False
case _:
assert_never(split)
allowed = may_fit_preprocessor("train")
The function returns True for "train" and False for the other declared
splits. Adding "holdout" to Split without updating the match produces a checker
error at assert_never: a possible value remains unhandled.
This checks completeness of the decision, not data provenance. A dataset named
"train" can still contain the wrong records. Unchecked input reaching
assert_never also fails at runtime.
An undefined metric is an outcome
Project overview and reading path
A report calculates precision from true positives and false positives. If there are no predicted positives, the score is undefined. A real score of zero means something different: predictions were made, but none was correct.
Typical Python
def precision(true_positives: int, false_positives: int) -> float:
total = true_positives + false_positives
return true_positives / total if total else 0.0
undefined = precision(0, 0)
incorrect = precision(0, 12)
Both (0, 0) and (0, 12) return 0.0. A caller cannot tell an undefined
score from a valid zero.
Alternative
def precision(tp: int, fp: int) -> Result[float, NoPredictedPositives | InvalidCounts]:
if tp < 0 or fp < 0:
return Err(InvalidCounts(tp, fp))
if tp + fp == 0:
return Err(NoPredictedPositives())
return Ok(tp / (tp + fp))
outcome = precision(0, 0)
match outcome:
case Ok(value=score):
report = f"{score:.3f}"
case Err(error=NoPredictedPositives()):
report = "undefined: no predicted positives"
case Err(error=InvalidCounts()):
report = "invalid counts"
case _:
assert_never(outcome)
This uses the Result pattern defined in the preceding
lesson. The complete example also matches both error variants.
precision(0, 0) returns Err(NoPredictedPositives()); precision(0, 12)
returns Ok(0.0). Formatting produces "undefined: no predicted positives"
and "0.000" respectively.
The checker rejects assigning the Result directly to float. The caller must
match both outcomes before using the score, and assert_never detects a newly
added outcome variant. A missing score is a supported outcome of this calculation.
Negative counts return Err(InvalidCounts(...)); callers must handle that
case too. The caller must still ensure both counts describe the same population.
The type checker cannot prove that condition or the numerical correctness of
the formula.
State-dependent APIs
Project overview and reading path
An email draft has text but no recipient. Sending it before choosing a recipient is a mistake the caller should discover while writing code.
Typical Python
class Email:
def __init__(self, body: str) -> None:
self.body = body
self.recipient: str | None = None
def send(self, deliver: Callable[[str, str], None]) -> None:
if self.recipient is None:
raise ValueError("recipient required")
deliver(self.recipient, self.body)
Email("Hello").send(record_delivery)
The checker accepts send because the method exists on every Email. The
missing recipient is discovered only when this call runs.
Alternative
@dataclass(frozen=True)
class DraftEmail:
body: str
def to(self, recipient: str) -> "AddressedEmail":
return AddressedEmail(recipient, self.body)
@dataclass(frozen=True)
class AddressedEmail:
recipient: str
body: str
def send(self, deliver: Callable[[str, str], None]) -> None:
deliver(self.recipient, self.body)
DraftEmail("Hello").to("reader@example.com").send(record_delivery)
DraftEmail("Hello").send(record_delivery) is now rejected as
missing-attribute. Calling .to("reader@example.com") returns an
AddressedEmail, which can be sent. The in-memory delivery function records
("reader@example.com", "Hello").
The type distinction ensures a recipient field is present; it does not validate
the address or prove delivery succeeds. Validate an external address at the
boundary and use a typed outcome for delivery failures
the caller can handle. The original draft remains usable after .to(); Python
does not enforce Rust-style moves.
Preserve types with generics
Project overview and reading path
A helper selects the first job from a batch. Callers need its job type, and selection requires at least one item.
Typical Python
def first(items):
return items[0]
job = first(jobs)
empty_job = first(())
The helper has no declared relationship between its input and output types.
Callers rely on knowing what jobs contains. An empty collection also fails with
IndexError when selected.
Alternative
T = TypeVar("T")
@dataclass(frozen=True)
class Batch(Generic[T]):
head: T
rest: tuple[T, ...] = ()
def first(batch: Batch[T]) -> T:
return batch.head
job = first(Batch(Job("report")))
first(Batch(Job("report"))) returns a Job. Assigning it to str is rejected;
T connects the stored item type to the return type. Requiring head also makes
Batch() a missing-argument error before execution.
The example selects Job(name="report"). An external collection can still be
empty: its boundary must handle absence before constructing the batch. A required
first item is useful only after that decision; an unchecked items[0] would merely
move the runtime failure elsewhere.
Immutable fields
Project overview and reading path
Several components share feature configuration. One component must not silently change the features seen by another.
Typical Python
from dataclasses import dataclass
@dataclass(frozen=True)
class Config:
features: list[str]
config = Config(["height", "width"])
config.features.append("area")
Freezing the record blocks field reassignment but permits mutation of the list. The append succeeds and changes shared state.
Alternative
@dataclass(frozen=True)
class Experiment:
seed: int
features: tuple[str, ...]
config = Experiment(seed=17, features=("height", "width"))
expanded_features = config.features + ("area",)
The tuple field has no .append, so that operation is rejected as
missing-attribute. Frozen fields reject reassignment, and Final protects
the default binding in checked code. The caller’s expanded_features is a new
tuple; config.features still contains only ("height", "width").
Immutability is shallow. A frozen record containing a mutable array still exposes that array’s mutation. Here each feature name is a string, so a tuple fits the actual contract. Environment parsing is a separate boundary.
Small protocols
Project overview and reading path
A preprocessing helper accepts interchangeable transforms. Each must return a numerical vector; a method with the right name but the wrong return type is insufficient.
Typical Python
def prepare(values, transform):
return transform.transform(values)
class Describe:
def transform(self, values):
return f"{len(values)} features"
features = prepare((1.0, 2.0), Describe())
total = sum(features)
The helper returns "2 features" where later code expects numbers. sum(features)
raises TypeError at runtime. The interface is implicit, so the mismatch survives
until the caller uses the value.
Alternative
class FeatureTransform(Protocol):
def transform(self, values: Features) -> Features: ...
def prepare(values: Features, transform: FeatureTransform) -> Features:
return transform.transform(values)
features = prepare((1.0, 2.0), Identity())
total = sum(features)
Identity is accepted, returns (1.0, 2.0), and gives total == 3.0.
Passing Describe is rejected as bad-argument-type: its return type violates
FeatureTransform.
Implementations satisfy the protocol structurally, without inheriting from it.
Positional-only arguments avoid requiring identical parameter names.
The checker does not prove preserved length, purity, or failure behavior. Test those contracts. Use a protocol where substitution is needed, not for every helper.
Framework inheritance is a separate requirement. For example, nn.Module supplies
PyTorch parameter registration; a protocol cannot replace that lifecycle. Keep
the framework base where required and expose only the capability a caller needs.
Compose interchangeable operations
Project overview and reading path
A processing pipeline supports independently added operations. Scaling and clipping should compose without a new branch in the pipeline for every operation.
Typical Python
def apply(values, operation):
if operation == "scale":
return tuple(value * 2 for value in values)
if operation == "clip":
return tuple(max(-1, min(1, value)) for value in values)
raise ValueError("unknown operation")
processed = apply(apply((-2.0, 0.25, 3.0), "scale"), "clip")
The nested calls produce (-1.0, 0.5, 1.0), but every new operation requires
editing the dispatcher. An unknown string raises ValueError only when called;
the selector hides which behavior and configuration an operation provides.
Alternative
class Transform(Protocol):
def transform(self, values: Features) -> Features: ...
class Pipeline:
def __init__(self, stages: tuple[Transform, ...]):
self.stages = stages
def transform(self, values: Features) -> Features:
for stage in self.stages:
values = stage.transform(values)
return values
pipeline = Pipeline((Scale(2.0), Clip(1.0)))
processed = pipeline.transform((-2.0, 0.25, 3.0))
The protocol lesson explains why an incompatible implementation is rejected. The complete example adds a typed registry lookup.
The example scales (-2.0, 0.25, 3.0) by two and clips to one, producing
(-1.0, 0.5, 1.0). New implementations satisfy Transform and are composed at
the entry point. Passing Describe, which returns text, is a checker error.
Unknown registry names are explicit outcomes; a caller must handle them before
using the transform. Mapping restricts this interface’s operations, but does
not freeze the underlying dictionary.
The protocol does not prove length, finiteness, or numerical correctness. Validate those requirements separately when they matter. Discovered implementations also need a typed SDK boundary.
Typed SDK boundaries
Project overview and reading path
An application calls an untyped prediction SDK. The SDK can mutate its request, fail, or return malformed data. Application code needs a checked outcome.
Typical Python
prediction = sdk.predict({"instances": [0.2, 0.8]})
confidence = prediction["confidence"]
The caller assumes success and a valid schema. Missing fields, invalid confidence, or SDK exceptions are discovered only when the application consumes them.
Alternative
PredictOutcome = Prediction | CallFailed | InvalidResponse
def invoke(request: PredictRequest) -> PredictOutcome:
payload = encode_request(request)
try:
response: object = candidate(payload)
except Exception as error:
return CallFailed(error)
return _parse_response(response)
outcome = invoke(PredictRequest(features=(0.2, 0.8)))
match outcome:
case Prediction(confidence=confidence):
message = f"confidence {confidence:.3f}"
case CallFailed(cause=cause):
message = f"SDK error: {type(cause).__name__}"
case InvalidResponse(reason=reason):
message = f"bad response: {reason}"
case _:
assert_never(outcome)
The adapter owns the uncertain call and validates its response. Its typed failure variants follow Result and match; the complete example also handles binding a dynamically discovered callable.
The demo returns Prediction(label="positive", confidence=0.8).
Binding may first return InvalidBinding; after matching a valid Predictor,
the call returns Prediction | CallFailed | InvalidResponse. Direct
.confidence access is rejected until the caller narrows the outcome.
Copying the payload also keeps SDK mutation away from the typed request.
Validation still happens at runtime. Confidence must be finite and in [0, 1];
text, booleans, NaN, and out-of-range values are rejected. Strict float validation
accepts integer endpoints. Existing model instances are revalidated too.
Why catch Exception here?
This SDK offers no reliable exception taxonomy. The adapter catches Exception
only around the vendor call, retaining its cause as CallFailed; encoding and
validation bugs outside that call remain visible. It does not catch
KeyboardInterrupt or SystemExit, stop hangs, or contain native crashes.
Timeouts, retries, and process isolation remain caller policies.
bind_vendor accepts object only at this unavoidable library seam. A
noncallable candidate returns InvalidBinding. callable does not prove the
signature: a mismatch becomes CallFailed(TypeError(...)).
Unknown values stay inside the adapter, which returns precise types.
hide_input_in_errors is not general redaction; apply reporting policy at the caller.
See declared fields for avoiding reflection in ordinary logic.
Declared fields instead of reflection
Project overview and reading path
A service reads a retry limit from a known configuration. Renaming or misspelling a field should be caught during development.
Typical Python
retries = getattr(config, "retrise", 3)
The typo silently selects the fallback. Attribute names passed as strings hide the intended field contract.
Alternative
from dataclasses import dataclass
@dataclass(frozen=True)
class Config:
retries: int = 3
config = Config()
retries = config.retries
Direct config.retrise is rejected as missing-attribute; the valid access
returns 3. The default belongs in the model, where all callers share its policy.
Keep getattr, setattr, hasattr, delattr, and equivalent reflection out of
ordinary application logic. Use a protocol for required capabilities and a typed
registry for selecting handlers. hasattr does not establish a method signature.
Necessary framework adapters and tests may use reflection locally, then return
precise types. This is a design policy, not a claim that checkers reject all reflection.
Settings at startup
Project overview and reading path
A service takes its worker count from the environment. Every component should use the same validated setting.
Typical Python
import os
workers = int(os.environ.get("APP_WORKERS", "4"))
The conversion accepts negative counts and leaves the schema and defaults scattered across read sites.
Alternative
from dataclasses import dataclass
from typing import Annotated, TypeAlias, assert_never
from pydantic import Field, ValidationError
from pydantic_settings import BaseSettings, SettingsConfigDict, SettingsError
class Settings(BaseSettings):
model_config = SettingsConfigDict(env_prefix="APP_")
workers: Annotated[int, Field(gt=0)] = 4
@dataclass(frozen=True)
class InvalidSettings:
reason: str
SettingsResult: TypeAlias = Settings | InvalidSettings
def load_settings() -> SettingsResult:
try:
return Settings()
except (ValidationError, SettingsError) as error:
return InvalidSettings(str(error))
outcome = load_settings()
match outcome:
case Settings(workers=workers):
pass
case InvalidSettings(reason=reason):
pass
case _:
assert_never(outcome)
With APP_WORKERS set to "4", matching the success variant binds integer
workers == 4. A negative or malformed value returns InvalidSettings.
The checker rejects using .workers on the unhandled SettingsResult, or a
misspelled field on Settings. Parsing and range checks still happen at runtime.
Load settings once at the entry point and pass values to components. Explicit
arguments override environment values; a dotenv file must be selected deliberately.
SettingsError from malformed complex settings is also translated at this
boundary. Unexpected defects still surface as exceptions.
A typed CLI schema
Project overview and reading path
A command accepts a positive worker count. Its application code should use a declared field, and CLI validation should share the same schema.
Typical Python
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("--workers", type=int, default=4)
options = parser.parse_args()
workers = options.worker_count
The namespace does not declare the expected field for the checker. The typo fails at runtime, and type=int alone accepts a negative worker count.
Alternative
from typing import Annotated
from pydantic import Field
from pydantic_settings import BaseSettings, CliApp
class Arguments(BaseSettings):
workers: Annotated[int, Field(gt=0)] = 4
options = CliApp.run(Arguments, cli_args=["--workers", "4"])
workers = options.workers
The explicit argument list produces workers == 4. options.worker_count is
now a checker error. Invalid ranges are rejected by runtime parsing; typing does
not inspect arbitrary command-line text.
Keep command dispatch and output at the entry point. CliSubCommand,
CliPositionalArg, and CliImplicitFlag extend the same typed schema when needed.
The example shows the argument flow on the page; no terminal invocation is required.
Narrowing through a predicate
Project overview and reading path
A helper recognizes nonempty text in a str | int value. Its caller wants to strip the text after the check.
Typical Python
def is_nonempty_text(value: str | int) -> bool:
return isinstance(value, str) and bool(value.strip())
def normalize_name(value: str | int) -> str:
if is_nonempty_text(value):
return value.strip()
raise ValueError("name must be nonempty text")
name = normalize_name(7)
The checker rejects .strip() because a plain bool return does not tell it
which type the predicate recognized. For 7, the caller also gets an unannounced
ValueError at runtime.
Alternative
def is_nonempty_text(value: str | int) -> TypeGuard[str]:
return isinstance(value, str) and bool(value.strip())
def normalize_name(value: str | int) -> str | InvalidName:
if is_nonempty_text(value):
return value.strip()
return InvalidName(value)
outcome = normalize_name(" training ")
match outcome:
case str() as name:
normalized = name
case InvalidName():
normalized = "rejected"
case _:
assert_never(outcome)
The failure is a typed outcome as in Result and match.
TypeGuard[str] communicates the narrowing. normalize_name(" training ")
returns "training", and the checker accepts .strip() in the guarded branch.
The return type also forces callers to distinguish InvalidName before calling
string methods. For a single use, an inline type check may be simpler.
The checker trusts the guard’s author; it does not prove the predicate correct or
encode nonemptiness in str. Invalid input returns InvalidName; external schema
validation belongs at a boundary.
Handle a checker defect locally
Project overview and reading path
A boundary receives an unknown mapping. Valid Python extracts a text field, but Pyrefly 1.3.1 rejects its mapping pattern. The goal is to isolate the checker defect without weakening the application’s checks.
Reproduction
def record_name(payload: object) -> str | None:
match payload:
case {"name": str(name)}:
return name.strip()
return None
name = record_name({"name": " training "})
For {"name": " training "}, Python returns "training". The pinned checker
reports not-callable at the case, referring to __getitem__ and Never.
The object parameter isolates an unknown-library boundary; it is not a suggested
application record type.
Scoped repair
def record_name(payload: object) -> str | None:
match payload:
# Pyrefly 1.3.1 mapping-pattern defect; remove once fixed.
# pyrefly: ignore[not-callable]
case {"name": str(name)}:
return name.strip()
return None
name = record_name({"name": " training "})
Only the reproduced diagnostic is suppressed. Unrelated bad assignments remain errors. The verification suite checks both facts on temporary inputs, along with the runtime result. An upgrade fixing the defect must trigger removal of the workaround.
Prefer a supported annotation or guard when it expresses the same contract clearly. Do not change accepted inputs, error behavior, or coercion just to satisfy one checker version. A suppression needs runtime evidence and a removal condition; unknown external schemas still need validation.
Correct an inaccurate stub
Project overview and reading path
A dependency returns the output name "embedding", but its type stub says the return value is an integer.
Typical Python
# vendor.pyi
def output_name() -> int: ...
# caller
name: str = output_name()
The checker reports bad-assignment even though the runtime result is a string.
Alternative
# corrected vendor.pyi
def output_name() -> str: ...
# unchanged caller
name: str = output_name()
The same caller is now accepted. Repairing the dependency contract preserves both runtime behavior and meaningful static checking.
Base the correction on verified behavior of the supported version. Keep local stubs in the project’s configured search path, preserving the API surface in use. Do not claim an unknown payload is a trusted record just to silence a diagnostic; validate it at a boundary.
A cast asserts; it does not validate
Project overview and reading path
An external JSON value must become a job record before application code reads its fields.
Typical Python
from typing import cast
decoded_payload = {"name": "report"}
metadata = cast(JobMetadata, decoded_payload)
workers = metadata.workers
The checker trusts this assertion even if required keys are missing. The cast
performs no validation or conversion: metadata is still a dictionary, so
reading .workers raises AttributeError at runtime.
Alternative
outcome = parse_metadata('{"name": "report"}')
match outcome:
case JobMetadata(workers=workers):
result = f"{workers} workers"
case InvalidMetadata(reason=reason):
result = f"rejected: {reason}"
case _:
assert_never(outcome)
The record parser validates the wire input before returning
JobMetadata. This missing-worker input returns InvalidMetadata, which the
caller handles before accessing workers.
The improvement is runtime evidence behind the annotation, not stronger checking
from the cast itself.
A narrow cast can be justified when an invariant is already established but the checker cannot express it. Keep the evidence and cast together at that boundary; do not spread an unsupported assertion across callers.
Design and review checklist
Project overview and reading path
Use this as a review reference after reading the relevant lessons. Apply only items that fit the change. Each link explains the rationale, costs, and examples.
Choose the contract
- Name a concrete mistake the design prevents; use the simplest representation that preserves the contract. Purpose
- Give shared records precise fields. Keep required fields required; parse dates and timestamps at the boundary. Data modeling
- Preserve relationships between arguments and results, and encode structural preconditions where useful. State, generics
Handle outcomes and alternatives
- Declare supported recovery decisions as typed outcomes. Document why any
remaining exceptions must unwind the operation; a
Raises:section is not a checked failure contract. Failure contracts - Narrow outcomes before consuming payloads and check closed unions with
assert_never. Avoid unchecked unwraps and discarded failures. Result handling - Distinguish unavailable values from valid zero or emptiness with typed outcomes. Undefined metrics
- Retain the diagnostic details callers need without turning programming defects into routine failures. Result handling
Validate at boundaries
- Validate external data with explicit coercion, missing-field, and extra-field policies; expose precise types afterward. Keep unknown library values inside adapters. Boundary validation
- Use declared fields and typed interfaces in application logic. Keep necessary reflection local to documented adapters or tests. Dynamic access
- Keep configuration and CLI constraints consistent, and side effects at the application edge. Settings, CLIs
- Define behavior for malformed input at the typed boundary. SDK adapter
Keep components and state understandable
- Use small protocols where substitution is needed. Implementations and wrappers must preserve error and mutation contracts. Composition
- Model state-dependent operations explicitly and account for shallow immutability and aliases. State, immutability
- In numerical code, keep native arrays and tensors. Check numerical behavior separately from static contracts. Limits
Verify the promises
- Check passing code and the intended static rejections. Keep workarounds narrow and supported by evidence. Static evidence, checker limitations
- Test observable behavior and the promised checker rejection. Reading examples
- Distinguish static guarantees, runtime validation, and conventions. Do not infer exception freedom, ownership, or scientific correctness from passing types. Benefits and limits
Use the guide in your coding agent
Project overview and reading path
Give Codex, Claude Code, or Cline this message:
Install the Rustic Python skill by reading and following:
https://raw.githubusercontent.com/Red-Eyed/rustic-python/main/INSTALL.md
The agent checks the repository revision, downloads the matching references, selects its own skill directory, and verifies the installed files. You do not need to clone this repository, install uv or just, or run setup commands. Add “for this project only” to the message if you want a project-scoped installation. Installing opts into automatic reference updates before use. For older installed copies that lack this behavior, ask the agent to “update” once using the same link.
Read the installation instructions to see exactly what the
agent will do. The same portable SKILL.md, chapters, examples, and templates
work across all three hosts. This requires an agent with file and network tools;
the host may ask for its normal permissions.
Use it
In Codex, use $rustic-python review this API. In Claude Code, use
/rustic-python review this API. In Cline, select /rustic-python in chat
and request the review.
The skill loads relevant chapters on demand. Installing it does not load the whole book into every conversation or replace your project’s instructions. Its tool settings are templates to adapt, not instructions to overwrite your configuration. Installed references remain available offline.
Host documentation: Codex, Claude Code, and Cline.
Automatic updates
At the start of each task using the skill, the agent compares the installed Git
revision with GitHub main. It refreshes an unchanged managed installation before
reading chapters and rereads the new skill instructions. This is an agent workflow,
not a background service or a timer. It needs the host’s file and network tools.
Bundles contain rustic-python-source.json with the source commit, a dirty-build
flag, and file hashes. The agent uses a commit-pinned repository archive when the
Pages ZIP has not caught up. That fallback reflects main without claiming its
CI build has completed. Package versions alone are not a freshness check.
An update preserves the previous installation in a backup outside skill discovery. If files were edited, provenance is unknown, or the installation is a symlink, the agent preserves it and uses a freshly staged snapshot for the task instead. Explicitly requested replacement of an edited directory still requires a backup. An explicit pin overrides automatic updates. Offline or blocked checks keep the cached copy and disclose that freshness was not verified; they never silently label it current.
For contributors working from a checkout
The optional just install-skill recipe links a locally built bundle into
Codex’s personal skill directory. To select a different host or project location,
set SKILLS_DIR to that host’s skills directory. Keep the checkout while using
this linked installation; rebuilding refreshes its references. Automatic refresh
will not write through that link or update your checkout. The downloadable
snapshot used by the agent installation flow does not depend on a checkout.
Install Rustic Python — instructions for coding agents
When the user asks you to install Rustic Python, perform the installation below with your own file and terminal tools. Do not hand the user a list of shell commands. This installs reference material, not Python dependencies. Installing the skill opts into checking for and applying reference updates when it is used. An explicit user request to pin a revision or disable updates takes precedence.
Choose the destination
Use the host you are currently running in, not the model name or which programs happen to be installed. Install for that host only, unless the user explicitly asks for multiple hosts. Default to personal scope; honor a requested project scope or explicit destination. Ask only if the host or requested location cannot be determined from the conversation and environment.
| Host | Personal destination | Project destination |
|---|---|---|
| Codex | ~/.agents/skills/rustic-python/ | <project>/.agents/skills/rustic-python/ |
| Claude Code | ~/.claude/skills/rustic-python/ | <project>/.claude/skills/rustic-python/ |
| Cline | ~/.cline/skills/rustic-python/ | <project>/.cline/skills/rustic-python/ |
Expand ~ using the user’s home directory, including on Windows. If the host has
an explicitly configured skills directory, use it. These instructions target
coding agents with local file access; a browser-only chat cannot install local
files and should say so rather than claim success.
Check for updates before use
Check once at the beginning of each task using the skill, including when a new task starts in an existing conversation. Use the folder from which this skill was loaded; do not install a second copy into a different scope.
- Fetch
https://api.github.com/repos/Red-Eyed/rustic-python/commits/mainwith an HTTP client that checks status codes, a bounded timeout, and cache revalidation (Cache-Control: no-cache). Read the full 40-character hexadecimalsha. This is the target revision for this task. Use commit identity, not package versions or dates: documentation can change between version bumps. Before staging an update, readINSTALL.mdfromhttps://raw.githubusercontent.com/Red-Eyed/rustic-python/<full SHA>/INSTALL.mdand use that revision’s layout and update instructions. Keep the already resolved target SHA; do not restart the check recursively. - Read local
rustic-python-source.json. Its format isschema_version: 1,repository: "https://github.com/Red-Eyed/rustic-python",source: {"commit": "<full SHA>", "dirty": false}, andfiles, a mapping of relative POSIX file paths to SHA-256 hex digests. Verify file hashes before treating the installation as unchanged. Ignore only__pycache__,.pytest_cache,.ruff_cache, and.pycfiles. Added, changed, missing, or symlinked reference files count as local modifications. The metadata file itself is excluded from its hash map. Digests detect edits, not authenticity. - If the repository matches,
source.dirtyis false, the commit matches the target, and the files are unchanged, continue using this copy. Otherwise stage the target using the next section. Missing or invalid metadata means unknown provenance, not a current installation. - Automatically refresh an unchanged managed installation, then reread the new
SKILL.mdand needed chapters. Keep this task’s check result so rereading does not trigger an update loop. Report an update with its old/new revisions.
For a user-pinned copy, honor the pin and identify it as pinned. For local edits, dirty builds, unknown provenance, or symlink installations, preserve the installed copy and use the freshly staged reference folder for this task instead; report its path and why the installed copy was not replaced. An explicitly requested update may replace a legacy or edited directory after backing it up. Never write through a symlink or change the user’s checkout automatically.
If GitHub is unavailable, rate-limited, permissions are denied, or no valid staged copy can be obtained, keep the existing copy and disclose its revision (if known) and that freshness could not be verified. Do not claim an offline copy is current. Do not loop on failures; retry on the next task. A first installation without a valid source fails clearly instead of reporting success. Check again when the user explicitly requests an update; otherwise do not poll throughout a task.
Stage the target revision
Perform downloads and file operations yourself. No clone, project dependencies, or execution of downloaded code is required.
- Fetch the ready-made bundle into a temporary directory:
https://red-eyed.github.io/rustic-python/downloads/rustic-python.zip.
Require a successful HTTP response. Inspect the archive before extracting:
reject absolute paths, parent traversal, symlinks, and duplicate destinations;
every entry must stay within its single
rustic-python/root. - Extract into staging. Accept this ZIP only when its metadata names the expected
repository, records the target commit with
dirty: false, and its complete file inventory and hashes match. Reject malformed metadata or escaping paths. - If Pages is unavailable or its ZIP does not match, fetch the repository archive
for the exact target SHA, never a moving
mainarchive:https://api.github.com/repos/Red-Eyed/rustic-python/zipball/<full SHA>. Follow GitHub’s archive redirect and enforce the same extraction rules, allowing its single generated repository root name. This fallback avoids waiting for Pages deployment; it is repository content, not a claim that CI has passed. - Assemble a fresh
rustic-python/folder from that archive: copyREADME.md,CHANGELOG.md,LICENSE,INSTALL.md,pyproject.toml,uv.lock,pyrefly.toml,ruff.toml,pytest.ini, and thedocs/,examples/,tests/directories. Omit caches,.pycfiles, andtests/test_distribution.py. Copyskills/rustic-python/SKILL.mdto the folder’s root asSKILL.md. Remove the README line beginning- [AGENTS.md]; do not copy repository maintenance instructions,.git,tools/, or host configuration. - For an archive-assembled copy, write the metadata format described above using
the target SHA,
dirty: false, and SHA-256 digests of all assembled files. Never relabel a stale Pages ZIP as the target revision. - Verify
SKILL.mdhasname: rustic-python, required directories and templates exist, and its relative references resolve inside staging. Do not install justSKILL.md. A malformed published bundle may fall back to the repository archive; a malformed fallback must stop the update and preserve the existing copy.
The target is the revision observed at the start of the check. If main advances
while downloading, finish this consistent snapshot and check again next task.
Do not combine chapters from different revisions.
Install or replace
For a new installation, choose the destination above. For an update, retain the existing destination. Finish staging and validation before touching it.
Create its parent if needed. For replacement, recheck the installed file hashes to catch edits made during downloading. If unchanged, move the old folder to a unique backup outside the host’s skill discovery directories, then move the complete staged folder into place. If replacement or final verification fails, restore the backup. Never merge files over the old directory: deleted chapters must disappear too. Serialize updates to the same destination; if another agent is updating it, use the staged snapshot for this task rather than racing it.
Verify the installed metadata, SKILL.md, and linked references at the final
path, then report the revision and backup path. Leave project settings,
AGENTS.md, CLAUDE.md, .clinerules, other skills, and dependency environments
untouched. Respect normal host file and network permissions; automatic refresh
does not bypass those permissions.
Report the result
Give the installed path, the source commit, the version from pyproject.toml, and one
invocation example:
- Codex:
$rustic-python review this preprocessing pipeline. - Claude Code:
/rustic-python review this preprocessing pipeline. - Cline: select
/rustic-pythonin chat, then request the review.
If the host exposes skill discovery, check that it lists rustic-python.
Otherwise report that the files are installed and ask the user to refresh the
skill list or restart the session only if it does not appear. Do not claim a
runtime activation test you did not perform.
Updates happen before skill use. Older copies without the refresh instructions need a one-time update using this link. The user may also request an immediate update or explicitly pin a revision. For removal, delete only the installed skill folder or symlink when requested.
Host references: Codex, Claude Code, and Cline.
GitHub references: commit lookup and commit-pinned archives.
Instructions for coding agents
Project overview and reading path
Use this page when applying the guide to another project. Read only the chapters relevant to the task. The guide recommends an approach; the target project’s requirements, supported Python versions, and tools take precedence. Installing the skill does not authorize replacing dependencies or refactoring unrelated code.
Apply the guide
- Name the concrete mistake to prevent and the caller’s supported decisions. Choose the smallest representation that makes incorrect use a checker error. Add a wrapper or interface only when it prevents a specific checked mistake.
- Preserve schemas and relationships in precise types. Validate unknown inputs at the boundary and contain library uncertainty there. Use the guide’s Pydantic and pydantic-settings examples as the reference implementation, respecting the target stack. See data modeling and settings.
- Declare recoverable failures as typed outcomes; let operations without a
supported recovery path unwind. Use the small custom
Ok[T] | Err[E]union when generic success/failure fits, withmatchandassert_never; no result base class, third-party result package, or unchecked unwrap is needed. ARaises:docstring is not a checked failure contract. See Result and match for recovery policy and the limits of this guarantee. Carry diagnostic details in typed error variants. - Keep reflection out of ordinary application logic. Use declared fields, explicit variants, and typed registries; contain necessary dynamic behavior in documented adapters or tests. See dynamic access.
- Introduce protocols at real substitution points and separate types for useful lifecycle restrictions. Preserve behavioral contracts as well as signatures. See composition, state, and generics.
- Verify the promised rejection with the project’s checker and the runtime obligations with relevant tests. Investigate checker limitations before adding suppressions.
Report the result
Explain which mistake is now rejected, what is validated at runtime, and what remains a convention or caller obligation. Static acceptance does not establish exception freedom, ownership, or numerical correctness. Keep native tensors in scientific code and test numerical behavior separately.
Use the review checklist for the final pass. Detailed chapter contracts take precedence over an abbreviated summary; do not invent a new framework or enforcement tool merely to apply this guide.
Growing the guide
Project overview and reading path
This appendix is for maintaining the book. Readers do not need its tools or commands.
Keep README.md as the project introduction and reading path.
Put detailed guidance in the relevant page under docs/. For a new topic, add a
focused page and link it from the entry point. Cross-page references should use
relative filenames and heading anchors rather than references to numbered sections.
Give each concept its own short page in this order:
- Model a concrete situation and the decision the caller needs to make.
- Show idiomatic Python for that situation. Use a brief
python,ignoresketch where the point is a conventional pattern, not a complete runnable source file. Do not invent strawman use ofobject,Any, orErras typical Python. - Show the better alternative in a short sketch containing only the contract and operation that change. Show a short caller use, including how it handles an expected failure or consumes the successful value. Link the complete checked source for details.
- After the sketch, state the observed outcome and why the change helps. Name the exact mistake the checker rejects; label runtime improvements honestly.
- Mention only limits and edge cases that change the design decision. If the pattern is checker-specific, record affected versions and removal conditions.
Each complete source needs a # rejected[diagnostic-kind]: statement that
isolates each claimed static rejection. Separate multiple kinds with commas.
The python,ignore fence highlights a contextual sketch; it does not claim that
the sketch is an independently checked complete listing. Keep the complete source
in the book as a link, without duplicating its full listing in the chapter.
Write for a reader who has not followed the project’s discussions. Explain the problem before prescribing a pattern, define unfamiliar terminology, and separate design principles from the reference tool stack. Keep development history in the changelog; turn past bugs into lasting lessons rather than review narratives. Keep one explanation and one useful example per idea. Remove repeated policy reminders, chapter summaries, and background that does not change a decision. Do not wrap ordinary identifiers in nominal types solely to demonstrate typing. Prefer clear names and keyword arguments; require a demonstrated recurring mix-up before adding distinct types. Keep numerical APIs on native tensors; do not manufacture tuple-based softmax implementations or nominal tensor wrappers to make a typing point. The detailed chapter owns the rationale. Keep the checklist and agent entry points short, linking to that chapter instead of repeating its full policy.
The book must be understandable without cloning the repository, installing Python, or opening a terminal. Show the relevant input, resulting value or failure, and explanation alongside each example. Source links provide optional provenance; they must not contain the only explanation or result. Never ask readers to run or uncomment code to discover the lesson. Keep setup and verification commands in this contributor appendix.
Every failure example needs a recovery contract. Use typed outcomes when callers
can retry, correct input, or reject a record and continue. For raising examples,
state why that operation must unwind. A Raises: docstring alone does not expose
a recoverable failure to the checker. See Result and match.
Put the executable source in examples/ and link it below its sketch with
[Source](../examples/name.py). Paths in documentation links are relative to
their page; shell commands run from the repository root. The tests discover
these files and reject missing or unlinked copies. Each module must contain at
least one rejected case. uv run --locked pytest checks independence, runtime
execution, and the marked negative examples. Add behavioral tests when a lesson
relies on runtime validation rather than static types alone.
Pytest lessons instead belong under tests/pytest_patterns/, with links to both
test modules and conftest.py. They are collected by pytest rather than run as
independent scripts, and do not need artificial static-rejection lines.
The source-link check discovers README.md and every Markdown page under
docs/, and covers both kinds of examples without duplicating source excerpts.
For tool upgrades, recheck the config files and examples together, then update the version requirements, project dependencies, and lockfile together.
Verify the maintained examples
Contributors use uv from the repository root. The lockfile supplies the tested environment; the project itself is not an installable runtime library.
uv run --python 3.11 --locked pytest
uv run --locked pyrefly check
uv run --locked ruff check .
uv run --locked ruff format --check .
These checks verify the examples, intended static rejections, and book source links. Keep the checker and editor environments aligned when investigating a difference. Ruff’s formatting check is read-only; formatting changes must be reviewed before committing.
Build the book and skill
docs/ remains the chapter source, and docs/SUMMARY.md defines reading order.
Add new chapters to that list; tests require every topic page to appear.
skills/rustic-python/SKILL.md is the short agent entry point. Its references are
bundled automatically from the chapters and executable examples.
just check # Lint, type-check, and test the guide and distributions
just book # Prepare sources, obtain mdBook, and build the HTML book
just serve # Build sources and preview the book in your browser
just bundle # Build the portable skill and ZIP without downloading mdBook
The build uses upstream mdBook binaries on macOS and Linux and keeps them under
ignored build/tools/. Output goes to build/book/; the skill goes to
build/skills/rustic-python/. Generated copies are never edited or committed.
After editing source chapters, restart just serve to refresh its staged input.
Build from a Git checkout: skill bundles record its HEAD, dirty state, and file
hashes in rustic-python-source.json. Dirty local builds are usable previews,
but must not be described as an exact published revision. Build from a clean,
stable checkout when producing a distributable snapshot.
The installed skill’s references do not require network access.
The GitHub Actions workflow runs checks and builds on pull requests and pushes
to main. Only successful builds on main deploy to GitHub Pages. The ZIP is
included in the site and uploaded as a workflow artifact. GitHub Pages must use
GitHub Actions as its publishing source in repository settings. Deployment access
is separate from building the book locally.
Further reading: Python typing specification, Rust enums and pattern matching, and Rust ownership.
Changelog
Changes are grouped by commit, newest first. Dates come from Git history.
e6a3f53 — 2026-09-30
Show caller behavior in lesson sketches — 0.8.5
- Add short calls and handled outcomes to the lessons so readers can see the runtime failure or value before the change and the caller benefit afterward.
- Remove the distinct-identifier lesson and its manual
NewTypewrappers; recommend clear names and keyword arguments unless repeated ID mix-ups justify distinct types.
d9e6f5b — 2026-09-30
Expected lookup failures as checked Results — 0.8.4
- Add a mapping lookup lesson that returns
Result[User, UserNotFound]for a missing key, so unchecked success access becomes a checker error. - Show when an expected failure belongs in
Resultand when raising for a broken invariant is analogous to Rust’spanic!.
60ea01b — 2026-09-29
Clearer state-dependent API example — 0.8.3
- Replace the centerer with a draft email that must acquire a recipient before
sendis available, making the premature call a checker error. - Demonstrate successful delivery through an in-memory callback and explain that address validation and delivery failures remain separate concerns.
bf83b9a — 2026-09-29
Concise alternatives with checked source links — 0.8.2
- Replace long inline example files with focused before/after sketches across the lessons; link the complete checked source bundled with the book.
- Refer readers to the
Resultand protocol lessons where later examples reuse those ideas, and keep source-link checks aligned with the shorter pages.
f94e1bb — 2026-09-29
Explain the Rust-inspired Result choice — 0.8.1
- Introduce
Resultandmatchbefore the other data contracts, and explain how they move handling of recoverable errors into checked code. - Explain the origin of
Result, the small Python union used here, and when directly named domain variants are clearer.
48f13ad — 2026-09-29
Type-checkable mistakes as the guide’s focus — 0.8.0
- State the book’s precise goal: move type-checkable mistakes into static checks, while leaving logical and numerical correctness to runtime tests.
- Remove lessons centered on runtime-only policy or repeated guarantees, along with their unused examples, tests, and iterator dependency.
- Make recoverable parser and vendor binding failures typed outcomes; simplify retained examples so each demonstrates a specific checker rejection.
2e8bfd1 — 2026-09-29
Highlight Python sketches — 0.7.2
- Give Python sketches syntax highlighting throughout the book while leaving the agent-install prompt as plain text.
- Document how contextual sketches differ from complete, source-linked examples.
28f2735 — 2026-09-29
Simpler validated record example — 0.7.1
- Replace the configured
TypedDictandTypeAdapterwith oneBaseModelin the job metadata lesson. The example now checks named attributes directly. - Preserve strict worker-count validation and the policy for extra JSON fields; update the corresponding tests and project version.
0872778 — 2026-09-29
Focused, example-first lessons — 0.7.0
- Split broad chapters into short concept pages. Each lesson now starts with a situation and familiar Python code, then shows an alternative and its benefit.
- Replace the undefined precision sentinel with a typed
Resulthandled bymatch, so zero and an unavailable score remain distinct. - Refresh book navigation, contributor guidance, and the portable skill’s topic links; keep the project and lockfile versions synchronized.
bb2eb76 — 2026-09-29
Learn static contracts through before/after code — 0.6.2
- Lead core lessons with code comparisons showing the specific misuse that moves from runtime to a checker error. Distinguish runtime-only improvements.
- Replace the nominal
Logits/Probabilitiessoftmax example withCustomerIdandOrderId: the checker rejects swapped identifiers and untagged integers. - Remove
scale_logitsandInferenceOutputfrom the inference example; retain validated settings and their plain-record handoff. Numerical APIs should use native tensors, not the removed tuple-based stand-ins.
47214d9 — 2026-09-29
A self-contained, shorter book — 0.6.1
- Show results and checker diagnostics directly in the chapters; remove reader instructions to clone, install tools, or execute examples.
- Confine verification commands to the contributor appendix and mark agent setup optional. Preserve complete code listings and their automated checks.
- Cut repeated introductions, policy reminders, and summaries across the guide; record concise, self-contained teaching as a contributor requirement.
b51da32 — 2026-09-29
A guide for general Python readers — 0.6.0
- Add prerequisites and a first-example walkthrough, align the README and book reading order, and explain ML concepts where specialist examples remain.
- Separate design principles from the recommended tool stack, replace discussion history with lasting lessons, and shorten the checklist and agent instructions.
- Clarify recovery contracts throughout the lessons. A
Raises:docstring does not expose recoverable failures to the type checker; typed outcomes do. - Change the introductory record example from
DatasetMetadatawithnum_classestoJobMetadatawith a positiveworkerscount. Readers reusing that example should update the type name and JSON field. - Change the generic example from
Batch(samples)toBatch(head, rest=()).firstnow returns the required first item without an empty-batch exception; checked construction rejects a missing first item. External collections still need an explicit empty-input policy before constructing the batch.
4a36831 — 2026-09-26
Recoverability determines failure representation — 0.5.3
- Use typed outcomes for supported recovery decisions, including retry, fallback, corrected input, and rejecting a record while processing continues.
- Reserve exceptions for operations with no meaningful recovery path that must unwind. Clarify that an outer boundary can still clean up, report, or isolate the failure; an exception does not require terminating the entire process.
- Replace the expected/unexpected-only rule throughout the guide and skill;
synchronize the project and lockfile version at
0.5.3.
12c0a73 — 2026-09-26
Expected failures as data — 0.5.2
- Reserve application exceptions for unexpected failures and violated internal assumptions. Represent anticipated failures as typed outcomes even when the caller chooses to stop rather than recover.
- Explain translating anticipated library errors at application boundaries and distinguish invalid external input from broken internal preconditions.
- Align the errors lesson, decision guide, checklist, and agent/skill instructions;
synchronize the project and lockfile version at
0.5.2.
1f0a733 — 2026-09-26
Statically checked failure contracts — 0.5.1
- Center result guidance on rejecting misuse before execution: unchecked success access, confused payload types, and incomplete variant handling.
- Recommend typed outcomes for expected failures callers should handle, replacing the blanket preference for exception propagation. Keep unexpected exceptions visible and distinguish static contracts from runtime input validation.
- Update the guide and skill to require evidence of rejected misuse; synchronize
the project and lockfile version at
0.5.1.
f3d2b1b — 2026-09-26
Optional custom result unions — 0.5.0
- Remove the returns and Expression dependencies and the Expression comparison.
The result lesson now defines independent frozen
Ok[T]andErr[E]records withResult[T, E]as a union alias, without a shared result base class. - Replace
Success/Failureand extraction methods withOk(value=...)andErr(error=...)pattern matching. Readers adapting the old lesson should handle each variant explicitly; the custom records provide no map or unwrap methods. - Verify generic payload contracts, rejection of unchecked field access, and exhaustive matching. Preserve exception tracebacks and notes in error payloads; callers explicitly raise from a stored exception when chaining is needed.
- Prefer exceptions for propagation and domain outcomes for failures callers inspect or collect. Present generic Result as an optional lesson rather than the default for every fallible function, including in agent and skill guidance.
- Synchronize the manifest and lockfile at
0.5.0; verify 220 tests on Python 3.11.
0e98f80 — 2026-09-26
Dynamic attribute access policy — 0.4.1
- Restrict
getattr,setattr,hasattr,delattr, and equivalent reflection in application logic; recommend declared fields, protocols, variants, and typed registries instead. - Explain documented exceptions for unavoidable integration boundaries and tests that deliberately exercise dynamic behavior. Identify the restriction as design and review guidance rather than a static guarantee.
- Include the policy in the boundary chapter, checklist, agent guide, and portable
skill; update the project and lockfile version to
0.4.1.
5fed7b8 — 2026-09-26
Precise application boundaries — 0.4.0
- Change
parse_metadata,parse_task, andprepare_inferenceto accept JSON text instead of arbitrary Python values. Callers copying these examples should pass the serialized input directly to Pydantic rather than decode it first. - Replace
VendorCallandpredict(request, vendor)with a boundPredictor: usevendor = bind_vendor(sdk_call), thenvendor(request). The callable returns validated outcomes; response parsing is private to the adapter. - Keep unknown library values inside integration seams and narrow the settings
validator’s output to
int. Move the mapping-pattern checker reproduction into the test harness and use a precise union in the runnable lesson. - Load the installation destination through pydantic-settings while preserving
the
SKILLS_DIRoverride and default location. - Update linked lessons, agent guidance, and static and runtime regression cases; verify 223 tests on Python 3.11 and synchronize the manifest and lockfile version.
8205663 — 2026-09-26
Design and review checklist
- Add paired Do / Avoid checks for data contracts, results and matching, boundary validation, component design, state, and testing.
- Link each topic to its detailed lesson and make the checklist accessible from the README, book navigation, and coding-agent instructions.
- Preserve guidance on acceptable simplifications and the limits of static
guarantees; update the project version to
0.3.3.
f8bae47 — 2026-09-26
Library result types and preserved failure diagnostics
- Replace custom generic Ok/Err/Result definitions with returns’
Result,Success, andFailure; update the guide and agent recommendations. - Explain that unchecked unwrapping can raise and that returns does not provide the exhaustive-matching guarantee of a closed Python union.
- Demonstrate typed mapping and explicit handling of both outcomes, with tests for invalid labels and unchecked extraction.
- Explain preserving caught exceptions, tracebacks, Python 3.11 notes, and data provenance; cover logging and the memory cost of retaining tracebacks.
- Add returns to the dependencies and update the project version to
0.3.2.
b9108ac — 2026-09-26
Automatic skill freshness checks
- Instruct installed skills to check GitHub before each task and refresh unchanged managed copies, then reread the updated guide.
- Include the source commit, dirty-build flag, and file hashes in skill bundles.
- Use a commit-pinned repository snapshot when the published ZIP lags behind; preserve local edits and report when offline freshness cannot be verified.
- Document backups, explicit pins, and the one-time update needed by older installations that lack refresh instructions.
- Verify bundle provenance and edit detection; update the version to
0.3.1.
06e8ad3 — 2026-09-26
Pydantic boundaries, settings, and inference contracts
- Use Pydantic for external schemas and demonstrate classification/regression discriminated unions with exhaustive downstream handling.
- Add pydantic-settings configuration, source-precedence tests, and guidance to build CLIs with typed argument models instead of hand-written argparse.
- Demonstrate validation before a plain NamedTuple/TypedDict inference handoff; keep Pydantic outside compiled execution. The scalar example does not claim verified PyTorch compilation support.
- Change SDK response validation to reject extra fields and accept integer
confidence endpoints as floats. Callers copying the example should remove
unexpected response keys. Label parsing now accepts integer-valued decimal
text such as
"7.0"; fractional labels remain invalid. - Document coercion, unchecked model construction, frozen-model limitations, and invalid settings and payload behavior, with 180 passing tests on Python 3.11.
- Replace Unreleased notes with commit-based history and update the project
version to
0.3.0.
bd956dc — 2026-09-26
Agent-operated installation and just recipes
- Add a single installation-instructions link for Codex, Claude Code, and Cline. The agent installs the ready-made bundle and verifies its references.
- Make agent-operated installation the primary README and book workflow.
- Replace Make with just for checks, book builds, and local skill installation; install just in GitHub Actions.
- Update the project version to
0.2.1.
20f1f38 — 2026-09-26
Online book and portable Codex skill
- Add an mdBook site with chapter navigation and search, built from the existing guide, plus GitHub Pages deployment after successful checks.
- Generate an offline skill bundle from the same chapters, examples, tests, and configuration templates; provide a downloadable ZIP and local installation.
- Check rendered links, chapter coverage, bundled references, and installation conflicts.
- Update the project version to
0.2.0.
1d92499 — 2026-09-26
Simpler tutorial configuration
- Consolidate dependencies into one list with
>=constraints; retain exact resolved versions inuv.lock. - Allow newer Ruff and Pyrefly versions in their standalone configurations.
- Move pytest settings into
pytest.iniand keep VS Code settings local. - Update the project version to
0.1.2.
abced5c — 2026-09-26
Initial practical typing guide
- Add a general-purpose Rust-inspired Python guide with an ML and data science bias, organized into topic pages with a README entry point.
- Cover records, sum types, errors, absence, state, generics, immutability, protocols, composition, and plugins using runnable Python 3.11 examples.
- Demonstrate untyped third-party boundaries, checker limitations, numerical correctness constraints, edge cases, and acceptable simplifications.
- Add pytest lessons for fixture dependency graphs, parametrization, cleanup, and anti-patterns, alongside guidance on supplementary libraries.
- Add strict Ruff and Pyrefly configurations, uv tooling, and tests for runtime behavior, expected type errors, and documentation synchronization.
- Add agent instructions and contribution guidance; introduce version
0.1.1.
9823996 — 2026-09-26
- Initialize the repository with the MIT license.