Type Hunter
Audit code for type design weaknesses — places where the type system could prevent bugs but doesn't because the
types are too loose, too broad, or too clever. The goal: types express domain intent precisely so misuse is caught
at check-time and refactors are safe.
This skill focuses on type design — whether the right types exist and are used. For enforcement questions
(whether existing invariants hold post-construction, loose optionality, defensive access, error suppression), see
invariant-hunter-py.
When to Use
- Reviewing type annotations for expressiveness and safety
- Tightening domain models before a refactor
- Reducing runtime validation by encoding invariants in the type system
- Preparing for stricter mypy/pyright configuration
- Auditing Python codebases transitioning from untyped to typed
Core Principles
Types encode domain rules. str says nothing about what a value represents. OrderId (via NewType) says it's
an order identifier — the type checker prevents mixing it with a user ID. Narrow types encode business meaning and
let the compiler catch misuse.
Make illegal states unrepresentable. If an Order can be in state "shipped" but tracking_number is
Optional[str], the type allows shipped orders without tracking numbers. Better: model ShippedOrder as a separate
dataclass that requires tracking_number: str.
Unions should be exhaustively handled. A Union[Success, Failure, Pending] is only as good as the handling of
each variant. If new variants are added, the type checker should flag unhandled cases — use assert_never() and
pattern matching with exhaustiveness checking.
Don't fight the type system. cast(), # type: ignore, and Any are escape hatches, not design tools. If
you need them frequently, the types are misaligned with the actual data flow. Fix the types, not the checker.
Type aliases should clarify, not obscure. UserId = str is documentation; NewType("UserId", str) is
enforcement. Callback = Callable[[int, str, bool, Optional[dict]], Awaitable[Optional[str]]] is a puzzle — name
the parameters via a Protocol.
What to Hunt
1. Primitive Obsession
Using str, int, float, dict, list where a domain type would be safer and more meaningful.
Signals:
- Functions accepting
str for IDs, codes, slugs, URLs, emails, paths
- Functions accepting
int for quantities, amounts, indices, timestamps
dict[str, Any] as a function parameter or return type (domain data without shape)
list[str] where the items have domain meaning (e.g., list of order IDs)
- Multiple parameters of the same primitive type that could be accidentally swapped:
def transfer(amount: int, from_id: str, to_id: str) — from_id and to_id are interchangeable to the type
checker
Action: Recommend NewType, dataclass, or TypedDict for domain-specific types. For ID types, NewType prevents
accidental mixing while having zero runtime cost.
2. Stringly-Typed APIs
Using string literals where Literal unions, Enum, or union types would provide type safety.
Signals:
str parameter with runtime checks like if status not in ("active", "inactive", "banned")
- Dict keys used as a discriminator without
Literal or TypedDict narrowing
- Error codes as bare strings instead of
Literal union or Enum
**kwargs: Any hiding structured options that should be typed
- Configuration dicts (
dict[str, Any]) instead of typed config dataclasses
Action: Replace with Literal["active", "inactive", "banned"] for small closed sets, or enum.Enum /
enum.StrEnum for larger sets with behavior. Use TypedDict or dataclass for structured dictionaries.
3. Over-Broad Unions and Optional Overuse
Unions that are wider than the actual possible values, or Optional used where the type definition should not
permit absence.
Boundary with invariant-hunter: type-hunter owns "this type should not be Optional — redesign the model."
invariant-hunter owns "given a correctly non-Optional type, downstream code still does unnecessary is not None
checks." If the type definition is wrong, it belongs here. If the type is right but consumers don't trust it, it
belongs in invariant-hunter.
Signals:
Optional[X] on a field that is always set after __init__ or after a specific lifecycle point
— the type definition itself is too loose
Union[str, int, float, bool, None] — too broad, indicates unclear data model
- Return type
Optional[X] where None means "not found" and also means "error" — conflated semantics
X | None passed through multiple layers requiring is not None checks at every level
Optional on fields that should force the caller to provide a value
Action: Narrow unions to the actual possible types. Split Optional into different return paths (e.g.,
raise exception for errors, return empty collection instead of None). Use @overload to narrow return types based on
input. Use separate dataclass variants instead of Optional fields that depend on state.
4. Structural vs. Nominal Confusion
Misuse of Protocol (structural typing) where a nominal type (ABC/base class) would be safer, or vice versa.
Signals:
Protocol used for domain types where accidental structural matches are dangerous (e.g., any object with a
.process() method satisfies Processor, even if it's unrelated)
- ABC/base class used for adapter interfaces where structural typing via
Protocol would allow easier extension
isinstance() checks in code that should use Protocol or union narrowing
- Overuse of
Any to bridge between incompatible types that should share a proper protocol
Action:
- Use nominal types (ABC, base class) for domain concepts where identity matters:
Order, User, Payment.
- Use structural types (
Protocol) for capability interfaces: Serializable, Renderable, Repository. These
describe what an object can do, not what it is.
5. Weak Discriminated Unions
Union types that lack a reliable discriminator field, forcing unsafe isinstance checks or hasattr() calls.
Signals:
Union[A, B, C] where there's no common field with Literal type to distinguish variants
isinstance() chains to narrow a union that should have a discriminator
hasattr() checks to determine which variant of a union is present
type: str field that should be type: Literal["a"] in each variant
Action: Add a Literal discriminator field to each variant. Use @dataclass variants with a shared type
field that narrows via Literal. Leverage pattern matching with match/case for exhaustive variant handling.
Example of well-discriminated union:
from dataclasses import dataclass
from typing import Literal, Union
@dataclass
class Success:
type: Literal["success"] = "success"
value: str
@dataclass
class Failure:
type: Literal["failure"] = "failure"
error: str
Result = Union[Success, Failure]
def handle(result: Result) -> None:
match result:
case Success(value=v):
print(v)
case Failure(error=e):
print(e)
6. Type Alias Soup
Type aliases that obscure rather than clarify, or missing aliases where they'd improve readability.
Signals:
- Deeply nested generics:
dict[str, list[tuple[int, Optional[str]]]] used inline instead of named
TypeAlias that just renames a primitive: Name: TypeAlias = str — provides documentation but no safety
- Multiple aliases for the same shape:
UserMap = dict[str, User] and UserDict = dict[str, User]
Callable types with 3+ parameters used inline: Callable[[int, str, bool, dict[str, Any]], Awaitable[str]]
Action:
- Replace
TypeAlias = str with NewType("X", str) when the alias should prevent mixing
- Name complex generics:
OrderIndex: TypeAlias = dict[str, list[Order]]
- For complex
Callable signatures, define a Protocol with __call__ for named parameters
- Eliminate duplicate aliases — one name per shape
7. Unsafe Narrowing and Type Guards
Incorrect or missing type narrowing that forces unsafe assertions or casts.
Signals:
cast() used to narrow a type that could be narrowed via isinstance(), pattern matching, or TypeGuard
# type: ignore on assignments that would pass with proper narrowing
- Missing
TypeGuard function for custom narrowing logic (e.g., checking a dict has certain keys)
assert isinstance(x, Foo) used for narrowing in production code (disabled by -O flag)
x: Any followed by field access without narrowing — no type checking on the access
Action: Replace cast() with isinstance() checks or TypeGuard functions. Use pattern matching for union
narrowing. Reserve cast() for situations where the type checker genuinely cannot infer the correct type and runtime
checking is impossible or too expensive.
8. Generic Type Misuse
Overuse, underuse, or incorrect use of TypeVar, Generic, and ParamSpec.
Signals:
TypeVar without constraints or bounds that should be constrained: T = TypeVar("T") where only int | str is
valid — should be T = TypeVar("T", int, str) or T = TypeVar("T", bound=Numeric)
Generic[T] class where T is only used once (doesn't relate input to output)
- Overly generic function where a simple union or overload would be clearer
- Missing
TypeVar where a function should preserve the input type in its return
ParamSpec / Concatenate used where a simpler Protocol would suffice
Action: Add bounds or constraints to TypeVar when only specific types are valid. Remove unnecessary generics
(use union or overload instead). Add generics when a function's return type truly depends on its input type.
Audit Workflow
Phase 1: Gain Context
- Resolve audit surface. The prompt may specify the scope as:
- Diff: files changed on the current branch vs base (
main/master)
- Path: specific files, folders, or layers
- Codebase: the entire project
If unspecified, default to codebase. For diff mode, resolve the file list:
BASE=$(git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed 's@^refs/remotes/origin/@@' || echo main)
SCOPE=$(git diff --name-only $(git merge-base HEAD $BASE)...HEAD)
Constrain all subsequent scans to the resolved surface.
- Identify the type checking configuration (mypy, pyright, pytype) and strictness level (
strict, basic,
off). Check pyproject.toml, mypy.ini, or pyrightconfig.json.
- Identify domain models — data classes, TypedDicts, Pydantic models. These are the priority for type design.
Phase 2: Scan for Type Design Signals
# Primitive-heavy function signatures
rg 'def \w+\(.*: (str|int|float|bool)(,|\))' --type py
# dict[str, Any] usage (untyped dicts)
rg 'dict\[str,\s*Any\]|Dict\[str,\s*Any\]' --type py
# Optional overuse
rg 'Optional\[|: .+ \| None' --type py
# Type escape hatches
rg '(: Any\b|cast\(|# type: ignore|# pyright: ignore)' --type py
# Stringly typed checks
rg '(if .+ (==|!=|in|not in) ["\x27]|\.get\(["\x27])' --type py
# isinstance chains (potential weak discrimination)
rg 'isinstance\(' --type py | head -30
# TypeVar and Generic usage
rg '(TypeVar|Generic\[|ParamSpec)' --type py
# NewType usage (or lack thereof)
rg 'NewType\(' --type py
# Enum usage
rg '(class \w+\(.*Enum\)|class \w+\(.*StrEnum\))' --type py
# Complex inline types (long Callable or deeply nested generics)
rg 'Callable\[\[.{40,}\]' --type py
Phase 3: Evaluate Domain Models
Review dataclasses, TypedDicts, and Pydantic models for:
- Primitive fields that should be
NewType or domain types
Optional fields that should be required or split into separate models
- Missing discriminator fields on union variants
- Mutable fields that should be
frozen=True
Review function signatures for:
str/int parameters that carry domain meaning
- Return types that are too broad (e.g.,
dict[str, Any] when the shape is known)
Any parameters or returns where a Protocol or union would be correct
Phase 4: Evaluate Type Safety Points
- For each
cast(), # type: ignore, Any: could proper narrowing or a better type design eliminate it?
- For each union: is there an exhaustive discriminator? Are all variants handled (check for
assert_never())?
- For each
Protocol: is structural typing appropriate, or should this be nominal?
- For each
TypeVar: are constraints or bounds appropriate?
Phase 5: Produce Report
Output Format
Save as YYYY-MM-DD-type-hunter-audit-{$LLM-name}.md in the project's docs folder (or project root if no docs folder
exists).
# Type Hunter Audit — {date}
## Scope
- Surface: {diff / path / codebase}
- Files: {count or list}
- Type checker: {mypy / pyright / pytype / none}
- Strictness: {strict / basic / off}
- Exclusions: {list}
## Findings
### Primitive Obsession
| # | Symbol | Location | Current Type | Suggested Type | Risk |
| - | ------ | -------- | ------------ | -------------- | ---- |
| 1 | `transfer()` args | file:line | `str, str` | `AccountId, AccountId` via NewType | High — IDs swappable |
### Stringly-Typed APIs
| # | Symbol | Location | String Usage | Suggested Type | Risk |
| - | ------ | -------- | ------------ | -------------- | ---- |
| 1 | `set_status()` | file:line | `status: str` checked at runtime | `Literal["active", "inactive"]` | Medium |
### Over-Broad Unions / Optional Overuse
| # | Symbol | Location | Current Type | Issue | Action |
| - | ------ | -------- | ------------ | ----- | ------ |
| 1 | `Order.tracking` | file:line | `Optional[str]` | Always set after shipping | Split into ShippedOrder |
### Structural vs. Nominal Confusion
| # | Symbol | Location | Current Design | Issue | Action |
| - | ------ | -------- | -------------- | ----- | ------ |
| 1 | `Processor` | file:line | Protocol | Domain type — accidental match risk | Use ABC |
### Weak Discriminated Unions
| # | Union | Location | Issue | Action |
| - | ----- | -------- | ----- | ------ |
| 1 | `Event` | file:line | No Literal discriminator | Add `type: Literal[...]` to each variant |
### Type Alias Issues
| # | Alias | Location | Issue | Action |
| - | ----- | -------- | ----- | ------ |
| 1 | `UserId = str` | file:line | Alias, not enforced | Use `NewType("UserId", str)` |
### Unsafe Narrowing
| # | Location | Escape Hatch | Action |
| - | -------- | ------------ | ------ |
| 1 | file:line | `cast(User, data)` | Use isinstance + TypeGuard |
### Generic Misuse
| # | Symbol | Location | Issue | Action |
| - | ------ | -------- | ----- | ------ |
| 1 | `process[T]` | file:line | Unbounded TypeVar, only int/str valid | Add constraint |
## Recommendations (Priority Order)
1. **Must-fix**: {primitive IDs, missing discriminators on critical unions, unsafe narrowing in domain logic}
2. **Should-fix**: {Optional overuse, stringly-typed APIs, broad unions}
3. **Consider**: {type alias cleanup, generic tightening, structural→nominal conversions}
Operating Constraints
- No code edits. This skill produces an audit report only. Implementation is a separate step.
- No empty sections. Include only categories with findings. Omit a heading, table, or list entirely when it would contain zero items — do not include empty tables, placeholder subsections, or negative statements like "no dead exports", "none found", or "no issues".
- Scope: type design only. Do not flag runtime enforcement gaps (→ invariant-hunter-py), structural complexity
(→ simplicity-hunter-py), naming issues (→ slop-hunter-py), test gaps (→ test-hunter-py), security
(→ security-hunter-py), module boundary violations (→ boundary-hunter-py), or documentation gaps
(→ doc-hunter-py). If a finding doesn't answer "could a better type prevent a bug here?", it doesn't belong
here.
- Evidence required. Every finding must cite
file/path.py:line with the exact type definition or usage.
- Pragmatism beats purity. Not every
str needs a NewType. Flag type weaknesses where a bug is likely — ID
parameters that could be swapped, states modeled with Optional that should be discriminated, unions without
exhaustive handling. Skip trivial cases where the primitive type is genuinely sufficient.
- Python version awareness. Note when suggestions require specific Python versions (e.g.,
match/case requires
3.10+, type statement requires 3.12+, TypeGuard requires 3.10+ or typing_extensions).
1---2name: type-hunter-py3description: Audit Python code for weak type design — primitive obsession, stringly-typed APIs, broad unions, structural vs nominal confusion, type aliases hiding intent, and models that fail to make illegal states unrepresentable. Use when: reviewing type annotations for expressiveness, tightening domain models, reducing runtime checks via the type system, or preparing for stricter mypy/pyright configuration. Reports omit empty sections — no placeholder headings, empty tables, or negative statements like "no issues found".4---56# Type Hunter78Audit code for **type design weaknesses** — places where the type system could prevent bugs but doesn't because the9types are too loose, too broad, or too clever. The goal: **types express domain intent precisely** so misuse is caught10at check-time and refactors are safe.1112This skill focuses on type *design* — whether the right types exist and are used. For *enforcement* questions13(whether existing invariants hold post-construction, loose optionality, defensive access, error suppression), see14invariant-hunter-py.1516## When to Use1718- Reviewing type annotations for expressiveness and safety19- Tightening domain models before a refactor20- Reducing runtime validation by encoding invariants in the type system21- Preparing for stricter mypy/pyright configuration22- Auditing Python codebases transitioning from untyped to typed2324## Core Principles25261. **Types encode domain rules.** `str` says nothing about what a value represents. `OrderId` (via `NewType`) says it's27 an order identifier — the type checker prevents mixing it with a user ID. Narrow types encode business meaning and28 let the compiler catch misuse.29302. **Make illegal states unrepresentable.** If an `Order` can be in state `"shipped"` but `tracking_number` is31 `Optional[str]`, the type allows shipped orders without tracking numbers. Better: model `ShippedOrder` as a separate32 dataclass that requires `tracking_number: str`.33343. **Unions should be exhaustively handled.** A `Union[Success, Failure, Pending]` is only as good as the handling of35 each variant. If new variants are added, the type checker should flag unhandled cases — use `assert_never()` and36 pattern matching with exhaustiveness checking.37384. **Don't fight the type system.** `cast()`, `# type: ignore`, and `Any` are escape hatches, not design tools. If39 you need them frequently, the types are misaligned with the actual data flow. Fix the types, not the checker.40415. **Type aliases should clarify, not obscure.** `UserId = str` is documentation; `NewType("UserId", str)` is42 enforcement. `Callback = Callable[[int, str, bool, Optional[dict]], Awaitable[Optional[str]]]` is a puzzle — name43 the parameters via a `Protocol`.4445## What to Hunt4647### 1. Primitive Obsession4849Using `str`, `int`, `float`, `dict`, `list` where a domain type would be safer and more meaningful.5051**Signals:**5253- Functions accepting `str` for IDs, codes, slugs, URLs, emails, paths54- Functions accepting `int` for quantities, amounts, indices, timestamps55- `dict[str, Any]` as a function parameter or return type (domain data without shape)56- `list[str]` where the items have domain meaning (e.g., list of order IDs)57- Multiple parameters of the same primitive type that could be accidentally swapped:58 `def transfer(amount: int, from_id: str, to_id: str)` — `from_id` and `to_id` are interchangeable to the type59 checker6061**Action:** Recommend `NewType`, dataclass, or `TypedDict` for domain-specific types. For ID types, `NewType` prevents62accidental mixing while having zero runtime cost.6364### 2. Stringly-Typed APIs6566Using string literals where `Literal` unions, `Enum`, or union types would provide type safety.6768**Signals:**6970- `str` parameter with runtime checks like `if status not in ("active", "inactive", "banned")`71- Dict keys used as a discriminator without `Literal` or `TypedDict` narrowing72- Error codes as bare strings instead of `Literal` union or `Enum`73- `**kwargs: Any` hiding structured options that should be typed74- Configuration dicts (`dict[str, Any]`) instead of typed config dataclasses7576**Action:** Replace with `Literal["active", "inactive", "banned"]` for small closed sets, or `enum.Enum` /77`enum.StrEnum` for larger sets with behavior. Use `TypedDict` or dataclass for structured dictionaries.7879### 3. Over-Broad Unions and `Optional` Overuse8081Unions that are wider than the actual possible values, or `Optional` used where the *type definition* should not82permit absence.8384**Boundary with invariant-hunter:** type-hunter owns "this type *should not be* `Optional` — redesign the model."85invariant-hunter owns "given a correctly non-Optional type, downstream code still does unnecessary `is not None`86checks." If the type definition is wrong, it belongs here. If the type is right but consumers don't trust it, it87belongs in invariant-hunter.8889**Signals:**9091- `Optional[X]` on a field that is always set after `__init__` or after a specific lifecycle point92 — the type definition itself is too loose93- `Union[str, int, float, bool, None]` — too broad, indicates unclear data model94- Return type `Optional[X]` where `None` means "not found" and also means "error" — conflated semantics95- `X | None` passed through multiple layers requiring `is not None` checks at every level96- `Optional` on fields that *should* force the caller to provide a value9798**Action:** Narrow unions to the actual possible types. Split `Optional` into different return paths (e.g.,99raise exception for errors, return empty collection instead of `None`). Use `@overload` to narrow return types based on100input. Use separate dataclass variants instead of `Optional` fields that depend on state.101102### 4. Structural vs. Nominal Confusion103104Misuse of `Protocol` (structural typing) where a nominal type (ABC/base class) would be safer, or vice versa.105106**Signals:**107108- `Protocol` used for domain types where accidental structural matches are dangerous (e.g., any object with a109 `.process()` method satisfies `Processor`, even if it's unrelated)110- ABC/base class used for adapter interfaces where structural typing via `Protocol` would allow easier extension111- `isinstance()` checks in code that should use `Protocol` or union narrowing112- Overuse of `Any` to bridge between incompatible types that should share a proper protocol113114**Action:**115116- Use **nominal types** (ABC, base class) for domain concepts where identity matters: `Order`, `User`, `Payment`.117- Use **structural types** (`Protocol`) for capability interfaces: `Serializable`, `Renderable`, `Repository`. These118 describe what an object can do, not what it is.119120### 5. Weak Discriminated Unions121122Union types that lack a reliable discriminator field, forcing unsafe isinstance checks or `hasattr()` calls.123124**Signals:**125126- `Union[A, B, C]` where there's no common field with `Literal` type to distinguish variants127- `isinstance()` chains to narrow a union that should have a discriminator128- `hasattr()` checks to determine which variant of a union is present129- `type: str` field that should be `type: Literal["a"]` in each variant130131**Action:** Add a `Literal` discriminator field to each variant. Use `@dataclass` variants with a shared `type`132field that narrows via `Literal`. Leverage pattern matching with `match/case` for exhaustive variant handling.133134Example of well-discriminated union:135136```python137from dataclasses import dataclass138from typing import Literal, Union139140@dataclass141class Success:142 type: Literal["success"] = "success"143 value: str144145@dataclass146class Failure:147 type: Literal["failure"] = "failure"148 error: str149150Result = Union[Success, Failure]151152def handle(result: Result) -> None:153 match result:154 case Success(value=v):155 print(v)156 case Failure(error=e):157 print(e)158```159160### 6. Type Alias Soup161162Type aliases that obscure rather than clarify, or missing aliases where they'd improve readability.163164**Signals:**165166- Deeply nested generics: `dict[str, list[tuple[int, Optional[str]]]]` used inline instead of named167- `TypeAlias` that just renames a primitive: `Name: TypeAlias = str` — provides documentation but no safety168- Multiple aliases for the same shape: `UserMap = dict[str, User]` and `UserDict = dict[str, User]`169- `Callable` types with 3+ parameters used inline: `Callable[[int, str, bool, dict[str, Any]], Awaitable[str]]`170171**Action:**172173- Replace `TypeAlias = str` with `NewType("X", str)` when the alias should prevent mixing174- Name complex generics: `OrderIndex: TypeAlias = dict[str, list[Order]]`175- For complex `Callable` signatures, define a `Protocol` with `__call__` for named parameters176- Eliminate duplicate aliases — one name per shape177178### 7. Unsafe Narrowing and Type Guards179180Incorrect or missing type narrowing that forces unsafe assertions or casts.181182**Signals:**183184- `cast()` used to narrow a type that could be narrowed via `isinstance()`, pattern matching, or `TypeGuard`185- `# type: ignore` on assignments that would pass with proper narrowing186- Missing `TypeGuard` function for custom narrowing logic (e.g., checking a dict has certain keys)187- `assert isinstance(x, Foo)` used for narrowing in production code (disabled by `-O` flag)188- `x: Any` followed by field access without narrowing — no type checking on the access189190**Action:** Replace `cast()` with `isinstance()` checks or `TypeGuard` functions. Use pattern matching for union191narrowing. Reserve `cast()` for situations where the type checker genuinely cannot infer the correct type and runtime192checking is impossible or too expensive.193194### 8. Generic Type Misuse195196Overuse, underuse, or incorrect use of `TypeVar`, `Generic`, and `ParamSpec`.197198**Signals:**199200- `TypeVar` without constraints or bounds that should be constrained: `T = TypeVar("T")` where only `int | str` is201 valid — should be `T = TypeVar("T", int, str)` or `T = TypeVar("T", bound=Numeric)`202- `Generic[T]` class where `T` is only used once (doesn't relate input to output)203- Overly generic function where a simple union or overload would be clearer204- Missing `TypeVar` where a function should preserve the input type in its return205- `ParamSpec` / `Concatenate` used where a simpler `Protocol` would suffice206207**Action:** Add bounds or constraints to `TypeVar` when only specific types are valid. Remove unnecessary generics208(use union or overload instead). Add generics when a function's return type truly depends on its input type.209210## Audit Workflow211212### Phase 1: Gain Context2132141. **Resolve audit surface.** The prompt may specify the scope as:215 - **Diff**: files changed on the current branch vs base (`main`/`master`)216 - **Path**: specific files, folders, or layers217 - **Codebase**: the entire project218 If unspecified, default to **codebase**. For diff mode, resolve the file list:219 ```bash220 BASE=$(git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed 's@^refs/remotes/origin/@@' || echo main)221 SCOPE=$(git diff --name-only $(git merge-base HEAD $BASE)...HEAD)222 ```223 Constrain all subsequent scans to the resolved surface.2242. Identify the type checking configuration (mypy, pyright, pytype) and strictness level (`strict`, `basic`,225 `off`). Check `pyproject.toml`, `mypy.ini`, or `pyrightconfig.json`.2263. Identify domain models — data classes, TypedDicts, Pydantic models. These are the priority for type design.227228### Phase 2: Scan for Type Design Signals229230```bash231# Primitive-heavy function signatures232rg 'def \w+\(.*: (str|int|float|bool)(,|\))' --type py233234# dict[str, Any] usage (untyped dicts)235rg 'dict\[str,\s*Any\]|Dict\[str,\s*Any\]' --type py236237# Optional overuse238rg 'Optional\[|: .+ \| None' --type py239240# Type escape hatches241rg '(: Any\b|cast\(|# type: ignore|# pyright: ignore)' --type py242243# Stringly typed checks244rg '(if .+ (==|!=|in|not in) ["\x27]|\.get\(["\x27])' --type py245246# isinstance chains (potential weak discrimination)247rg 'isinstance\(' --type py | head -30248249# TypeVar and Generic usage250rg '(TypeVar|Generic\[|ParamSpec)' --type py251252# NewType usage (or lack thereof)253rg 'NewType\(' --type py254255# Enum usage256rg '(class \w+\(.*Enum\)|class \w+\(.*StrEnum\))' --type py257258# Complex inline types (long Callable or deeply nested generics)259rg 'Callable\[\[.{40,}\]' --type py260```261262### Phase 3: Evaluate Domain Models2632641. Review dataclasses, TypedDicts, and Pydantic models for:265 - Primitive fields that should be `NewType` or domain types266 - `Optional` fields that should be required or split into separate models267 - Missing discriminator fields on union variants268 - Mutable fields that should be `frozen=True`2692702. Review function signatures for:271 - `str`/`int` parameters that carry domain meaning272 - Return types that are too broad (e.g., `dict[str, Any]` when the shape is known)273 - `Any` parameters or returns where a `Protocol` or union would be correct274275### Phase 4: Evaluate Type Safety Points2762771. For each `cast()`, `# type: ignore`, `Any`: could proper narrowing or a better type design eliminate it?2782. For each union: is there an exhaustive discriminator? Are all variants handled (check for `assert_never()`)?2793. For each `Protocol`: is structural typing appropriate, or should this be nominal?2804. For each `TypeVar`: are constraints or bounds appropriate?281282### Phase 5: Produce Report283284## Output Format285286Save as `YYYY-MM-DD-type-hunter-audit-{$LLM-name}.md` in the project's docs folder (or project root if no docs folder287exists).288289```md290# Type Hunter Audit — {date}291292## Scope293294- Surface: {diff / path / codebase}295- Files: {count or list}296- Type checker: {mypy / pyright / pytype / none}297- Strictness: {strict / basic / off}298- Exclusions: {list}299300## Findings301302### Primitive Obsession303304| # | Symbol | Location | Current Type | Suggested Type | Risk |305| - | ------ | -------- | ------------ | -------------- | ---- |306| 1 | `transfer()` args | file:line | `str, str` | `AccountId, AccountId` via NewType | High — IDs swappable |307308### Stringly-Typed APIs309310| # | Symbol | Location | String Usage | Suggested Type | Risk |311| - | ------ | -------- | ------------ | -------------- | ---- |312| 1 | `set_status()` | file:line | `status: str` checked at runtime | `Literal["active", "inactive"]` | Medium |313314### Over-Broad Unions / Optional Overuse315316| # | Symbol | Location | Current Type | Issue | Action |317| - | ------ | -------- | ------------ | ----- | ------ |318| 1 | `Order.tracking` | file:line | `Optional[str]` | Always set after shipping | Split into ShippedOrder |319320### Structural vs. Nominal Confusion321322| # | Symbol | Location | Current Design | Issue | Action |323| - | ------ | -------- | -------------- | ----- | ------ |324| 1 | `Processor` | file:line | Protocol | Domain type — accidental match risk | Use ABC |325326### Weak Discriminated Unions327328| # | Union | Location | Issue | Action |329| - | ----- | -------- | ----- | ------ |330| 1 | `Event` | file:line | No Literal discriminator | Add `type: Literal[...]` to each variant |331332### Type Alias Issues333334| # | Alias | Location | Issue | Action |335| - | ----- | -------- | ----- | ------ |336| 1 | `UserId = str` | file:line | Alias, not enforced | Use `NewType("UserId", str)` |337338### Unsafe Narrowing339340| # | Location | Escape Hatch | Action |341| - | -------- | ------------ | ------ |342| 1 | file:line | `cast(User, data)` | Use isinstance + TypeGuard |343344### Generic Misuse345346| # | Symbol | Location | Issue | Action |347| - | ------ | -------- | ----- | ------ |348| 1 | `process[T]` | file:line | Unbounded TypeVar, only int/str valid | Add constraint |349350## Recommendations (Priority Order)3513521. **Must-fix**: {primitive IDs, missing discriminators on critical unions, unsafe narrowing in domain logic}3532. **Should-fix**: {Optional overuse, stringly-typed APIs, broad unions}3543. **Consider**: {type alias cleanup, generic tightening, structural→nominal conversions}355```356357## Operating Constraints358359- **No code edits.** This skill produces an audit report only. Implementation is a separate step.360- **No empty sections.** Include only categories with findings. Omit a heading, table, or list entirely when it would contain zero items — do not include empty tables, placeholder subsections, or negative statements like "no dead exports", "none found", or "no issues".361- **Scope: type design only.** Do not flag runtime enforcement gaps (→ invariant-hunter-py), structural complexity362 (→ simplicity-hunter-py), naming issues (→ slop-hunter-py), test gaps (→ test-hunter-py), security363 (→ security-hunter-py), module boundary violations (→ boundary-hunter-py), or documentation gaps364 (→ doc-hunter-py). If a finding doesn't answer "could a better type prevent a bug here?", it doesn't belong365 here.366- **Evidence required.** Every finding must cite `file/path.py:line` with the exact type definition or usage.367- **Pragmatism beats purity.** Not every `str` needs a `NewType`. Flag type weaknesses where a bug is likely — ID368 parameters that could be swapped, states modeled with Optional that should be discriminated, unions without369 exhaustive handling. Skip trivial cases where the primitive type is genuinely sufficient.370- **Python version awareness.** Note when suggestions require specific Python versions (e.g., `match/case` requires371 3.10+, `type` statement requires 3.12+, `TypeGuard` requires 3.10+ or `typing_extensions`).