Skip to content

Typing Review Workflow

This workflow is distilled from practical typing modernization passes and is designed for latest-syntax-first upgrades.

Step-by-Step Process

  1. Identify the Python baseline from project config (requires-python, lint target version, toolchain constraints).
  2. Scan target files for legacy typing patterns and repeated opportunities.
  3. Apply highest-value modern syntax updates first:
  4. typing.List/typing.Dict -> built-in generics.
  5. Optional[T]/Union[A, B] -> T | None / A | B.
  6. Upgrade generic declarations to PEP 695 syntax where baseline allows:
  7. TypeVar module globals -> local type parameters in classes/functions.
  8. Tighten domain contracts where clear:
  9. replace unconstrained str with Literal[...] for finite known values.
  10. use Self for fluent APIs.
  11. Keep edits minimal and avoid behavior changes unless requested.
  12. Validate with lint and editor diagnostics.
  13. 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 TypeVar with 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).

  1. Prefer match over long if/elif ladders when each branch represents a distinct variant.
  2. For tagged unions, match the discriminant and extract payload fields in the same case.
  3. Add a default case _: branch with assert_never(...) to enforce exhaustiveness in static analysis.
  4. 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

  1. All edits are syntax-valid for the target Python versions.
  2. Lint and diagnostics pass for edited files.
  3. Runtime behavior is unchanged for modernization-only tasks.
  4. Recommendations cite authoritative sources.
  5. 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.