diff --git a/docs/skills/python-typing/SKILL.md b/docs/skills/python-typing/SKILL.md index 374b194..6b1eadc 100644 --- a/docs/skills/python-typing/SKILL.md +++ b/docs/skills/python-typing/SKILL.md @@ -73,6 +73,20 @@ Use these defaults unless a hard compatibility constraint prevents them: 5. Use `typing.Literal` when a finite value set is the real contract. 6. Remove legacy typing aliases and module-level `TypeVar` declarations when PEP 695 can replace them. 7. Keep runtime behavior unchanged unless the task explicitly requests behavior refactors. +8. Treat `typing.cast(...)` as a last resort, not a default fix for type-checker complaints. +9. Before adding a cast, prefer real narrowing (`isinstance`, `TypeIs`/`TypeGuard`), explicit control-flow checks, or small annotation refactors that preserve behavior. +10. Reject casts whose only purpose is to silence the checker without a clear runtime invariant. +11. For closed variant sets (`Literal`/`Enum`/tagged unions), prefer structural pattern matching with exhaustiveness checks (`assert_never`) for deterministic narrowing. + +## Cast Discipline + +Use this policy whenever a modernization pass encounters a potential cast: + +1. Confirm whether the checker can be satisfied with stronger narrowing first (for example `isinstance` or assertion-based narrowing). +2. If a cast is still necessary, keep it narrowly scoped to the exact expression rather than widening an entire variable flow. +3. Document the invariant that makes the cast valid in human terms, not just "type checker requires this". +4. Prefer fixing imprecise annotations at the source over stacking repeated casts downstream. +5. If multiple casts appear in one code path, treat that as a design smell and propose a structural typing fix. ## Completion Checks @@ -81,6 +95,7 @@ Use these defaults unless a hard compatibility constraint prevents them: 3. Public APIs are unchanged unless explicitly requested. 4. Feature-level recommendations include source links. 5. Any deferral is backed by a specific hard constraint (for example Python version floor). +6. New casts, if any, are minimal, justified by an explicit invariant, and not used as checker-silencing shortcuts. ## Output Contract diff --git a/docs/skills/python-typing/references/index.md b/docs/skills/python-typing/references/index.md index f3f9f72..6307ecd 100644 --- a/docs/skills/python-typing/references/index.md +++ b/docs/skills/python-typing/references/index.md @@ -7,6 +7,13 @@ Use this page as the canonical source index when making typing modernization rec - [Typing module documentation](https://docs.python.org/3/library/typing.html) - [Typing specification (typing.python.org)](https://typing.python.org/) - [Built-in types and generic aliases](https://docs.python.org/3/library/stdtypes.html) +- [Python language reference: `match` statement](https://docs.python.org/3/reference/compound_stmts.html#the-match-statement) +- [PEP 634: Structural Pattern Matching specification](https://peps.python.org/pep-0634/) +- [typing.cast reference (runtime no-op)](https://docs.python.org/3/library/typing.html#typing.cast) +- [Typing spec directives for `cast()`](https://typing.python.org/en/latest/spec/directives.html#cast) +- [Mypy type narrowing and casts guidance](https://mypy.readthedocs.io/en/stable/type_narrowing.html#casts) +- [Typing guide: exhaustiveness and `assert_never`](https://typing.python.org/en/latest/guides/unreachable.html#assert-never-and-exhaustiveness-checking) +- [Mypy: `Literal`/`Enum` exhaustiveness with `match`](https://mypy.readthedocs.io/en/stable/literal_types.html#exhaustiveness-checking) ## Tooling References diff --git a/docs/skills/python-typing/references/review-workflow.md b/docs/skills/python-typing/references/review-workflow.md index d83ce15..e1f1bcf 100644 --- a/docs/skills/python-typing/references/review-workflow.md +++ b/docs/skills/python-typing/references/review-workflow.md @@ -29,6 +29,46 @@ This workflow is distilled from practical typing modernization passes and is des - 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: + +```python +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.