3.4 KiB
3.4 KiB
Typing Review Workflow
This workflow is distilled from practical typing modernization passes and is designed for latest-syntax-first upgrades.
Step-by-Step Process
- Identify the Python baseline from project config (
requires-python, lint target version, toolchain constraints). - Scan target files for legacy typing patterns and repeated opportunities.
- Apply highest-value modern syntax updates first:
typing.List/typing.Dict-> built-in generics.Optional[T]/Union[A, B]->T | None/A | B.
- Upgrade generic declarations to PEP 695 syntax where baseline allows:
TypeVarmodule globals -> local type parameters in classes/functions.
- Tighten domain contracts where clear:
- replace unconstrained
strwithLiteral[...]for finite known values. - use
Selffor fluent APIs.
- replace unconstrained
- Keep edits minimal and avoid behavior changes unless requested.
- Validate with lint and editor diagnostics.
- Report applied changes, hard-blocker deferrals, and sources consulted.
Decision Points and Branching
- If Python baseline is below 3.12:
- use the newest syntax available under that baseline, and document exactly what blocked PEP 695.
- If a legacy annotation is public API and downstream tooling compatibility is unknown:
- still modernize syntax unless there is a confirmed breakage risk with a named downstream constraint.
- If replacing
TypeVarwith PEP 695 affects readability debates only:- still prefer PEP 695; readability preference alone is not a blocker.
- If a stricter type (for example
Literal) may reject existing runtime inputs:- apply only when the input contract is already finite; otherwise defer with a contract-change note.
Deterministic Narrowing with match
Use structural pattern matching when the domain is a closed set (for example tagged unions, enum dispatch, or finite literal variants).
- Prefer
matchover longif/elifladders when each branch represents a distinct variant. - For tagged unions, match the discriminant and extract payload fields in the same case.
- Add a default
case _:branch withassert_never(...)to enforce exhaustiveness in static analysis. - Keep patterns explicit and side-effect-light; avoid relying on bindings from failed matches.
Example with a tagged TypedDict union:
from typing import Literal, TypedDict, assert_never
class NewJobEvent(TypedDict):
tag: Literal["new-job"]
job_name: str
class CancelJobEvent(TypedDict):
tag: Literal["cancel-job"]
job_id: int
type Event = NewJobEvent | CancelJobEvent
def route(event: Event) -> str:
match event:
case {"tag": "new-job", "job_name": job_name}:
return f"enqueue:{job_name}"
case {"tag": "cancel-job", "job_id": job_id}:
return f"cancel:{job_id}"
case _:
assert_never(event)
This pattern makes narrowing deterministic per branch and surfaces missing variants as type-checker errors during review.
Quality Criteria
- All edits are syntax-valid for the target Python versions.
- Lint and diagnostics pass for edited files.
- Runtime behavior is unchanged for modernization-only tasks.
- Recommendations cite authoritative sources.
- Output clearly separates "changed now" from hard-blocked follow-up items.
Suggested Validation Commands
uv run ruff check <paths>uv run pytest -q(or targeted tests where available)
Use repository-preferred test invocation conventions when they differ.