hunter-party-py/type-hunter-py/SKILL.md
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.
npx skillsauth add skyosev/agent-skills type-hunter-pyInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
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.
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.
Using str, int, float, dict, list where a domain type would be safer and more meaningful.
Signals:
str for IDs, codes, slugs, URLs, emails, pathsint for quantities, amounts, indices, timestampsdict[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)def transfer(amount: int, from_id: str, to_id: str) — from_id and to_id are interchangeable to the type
checkerAction: Recommend NewType, dataclass, or TypedDict for domain-specific types. For ID types, NewType prevents
accidental mixing while having zero runtime cost.
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")Literal or TypedDict narrowingLiteral union or Enum**kwargs: Any hiding structured options that should be typeddict[str, Any]) instead of typed config dataclassesAction: 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.
Optional OveruseUnions 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 looseUnion[str, int, float, bool, None] — too broad, indicates unclear data modelOptional[X] where None means "not found" and also means "error" — conflated semanticsX | None passed through multiple layers requiring is not None checks at every levelOptional on fields that should force the caller to provide a valueAction: 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.
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)Protocol would allow easier extensionisinstance() checks in code that should use Protocol or union narrowingAny to bridge between incompatible types that should share a proper protocolAction:
Order, User, Payment.Protocol) for capability interfaces: Serializable, Renderable, Repository. These
describe what an object can do, not what it is.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 variantsisinstance() chains to narrow a union that should have a discriminatorhasattr() checks to determine which variant of a union is presenttype: str field that should be type: Literal["a"] in each variantAction: 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)
Type aliases that obscure rather than clarify, or missing aliases where they'd improve readability.
Signals:
dict[str, list[tuple[int, Optional[str]]]] used inline instead of namedTypeAlias that just renames a primitive: Name: TypeAlias = str — provides documentation but no safetyUserMap = 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:
TypeAlias = str with NewType("X", str) when the alias should prevent mixingOrderIndex: TypeAlias = dict[str, list[Order]]Callable signatures, define a Protocol with __call__ for named parametersIncorrect 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 narrowingTypeGuard 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 accessAction: 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.
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)TypeVar where a function should preserve the input type in its returnParamSpec / Concatenate used where a simpler Protocol would sufficeAction: 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.
main/master)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.strict, basic,
off). Check pyproject.toml, mypy.ini, or pyrightconfig.json.# 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
Review dataclasses, TypedDicts, and Pydantic models for:
NewType or domain typesOptional fields that should be required or split into separate modelsfrozen=TrueReview function signatures for:
str/int parameters that carry domain meaningdict[str, Any] when the shape is known)Any parameters or returns where a Protocol or union would be correctcast(), # type: ignore, Any: could proper narrowing or a better type design eliminate it?assert_never())?Protocol: is structural typing appropriate, or should this be nominal?TypeVar: are constraints or bounds appropriate?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}
file/path.py:line with the exact type definition or usage.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.match/case requires
3.10+, type statement requires 3.12+, TypeGuard requires 3.10+ or typing_extensions).development
Transforms vague feature ideas into precise, codebase-grounded technical requirements. Use when requirements are ambiguous/incomplete, the user struggles to describe behavior, terminology is unclear, or multiple concepts are mixed. Output is a requirements spec—NOT an implementation plan.
tools
Audit TypeScript type definitions for design debt — duplicated shapes, missing derivations, over-engineered generics, under-constrained type parameters, reinvented utility types, and disorganized type architecture. Type structure and maintainability, not type enforcement. Use when: reviewing type definitions for maintainability, reducing type duplication, simplifying over-engineered type-level logic, or reorganizing type architecture after growth.
development
Audit TypeScript test code for quality gaps — missing coverage on critical paths, brittle tests coupled to implementation, over-mocking, assertion-free tests, missing edge cases, and duplicated test setup. Focuses on test effectiveness, not production code structure. Use when: reviewing TypeScript test suites for reliability, reducing false-positive test failures, improving coverage of critical business logic, or cleaning up test debt.
tools
Audit TypeScript class and interface design for SOLID violations — god classes, rigid extension points, broken substitutability, fat interfaces, and concrete dependency chains. Focuses on responsibility assignment and abstraction fitness. Use when: reviewing class hierarchies, preparing for extension with new variants, reducing coupling between services, or improving testability of class-heavy code.