hunter-party-py/invariant-hunter-py/SKILL.md
Audit Python code for weak invariants — unnecessary casts, loose optionality, defensive None-checks masking missing guarantees, leaky tagged unions, error suppression, and runtime checks that the type system or construction boundaries should enforce. Use when: tightening post-construction guarantees, reducing type: ignore and cast() usage, reviewing dataclass/TypedDict optionality, auditing error-handling hygiene, or establishing a type-safety baseline before refactoring.
npx skillsauth add skyosev/agent-skills invariant-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 Python code to make the type system enforce invariants that are currently left to runtime, convention, or
cast()/# type: ignore assertions. The goal: guarantees established at construction boundaries hold throughout
downstream code, and consumers narrow via isinstance/match without casts.
This skill focuses on enforcement — whether invariants that should hold are actually enforced. For type design questions (primitive obsession, NewType opportunities, structural vs nominal choice, alias clarity, generic correctness), see type-hunter-py.
cast(), # type: ignore, and Any usage that compensates for loose upstream typesTypes are documentation that runs. Encode invariants in the type system. If it cannot be encoded without excessive complexity, validate at runtime with a clear error.
Resolve at construction boundaries. Defaults and validation belong where data is created or enters the system —
public API entry points, factories, __init__ methods. Downstream functions should require their inputs. If a
caller "always passes X", make X required and push the default to the construction boundary.
Every Optional is a branch. An Optional[T] field means T | None — the consumer must handle the absent
case. Only use Optional when the domain genuinely permits absence, not as a convenience for callers.
is not None checks are symptoms. None-checks have legitimate uses (truly optional data, external API
responses), but each occurrence is a branch the reader must reason about. In non-boundary code, they signal a type
that is too loose. The fix is tightening the upstream type, not adding defensive access.
Discriminants: single source of truth. The discriminant field must be exhaustive. Redundant fields carrying the
same information (e.g., kind vs is_active flag) are a drift risk — eliminate one or derive it.
cast() must be justified. Type casts should be minimized, but runtime-guarded casts are acceptable when
the cast immediately follows a runtime check and mypy's/pyright's control flow analysis cannot correlate the
discriminants. Do not blindly refactor into verbose alternatives that harm ergonomics without improving safety.
Fail fast. When an invariant is violated, raise immediately — do not silently return a default or catch-and-log. Invariant violations are programmer errors; they should crash loudly to surface bugs. For error handling design (exception hierarchy, chaining, try/except scope, silent suppression), see error-hunter-py.
Eliminate type-system bypasses. Any, cast(), # type: ignore, # pyright: ignore are escape hatches.
Each must be justified (why necessary), scoped (boundary layers only), and temporary (tracked as tech debt).
Not every finding requires action. Document these but do not flag as "must-fix":
| Pattern | When Acceptable |
| ------- | --------------- |
| Runtime-guarded cast() | Cast immediately follows a runtime check |
| Optional utility parameters | Helper accepting Optional when domain type requires |
| is not None at true boundaries | External API responses, user input, config defaults |
| Try/except at error boundaries | Top-level handlers, middleware with defined recovery |
| Type bypasses in boundary layers | JSON parsing, FFI, library workarounds — with comment |
cast() assertions and # type: ignore where narrowing should work or the type can be tightened.
Signals:
cast() not preceded by a runtime check validating the assertionassert x is not None used to satisfy the type checker where the type should be non-optional# type: ignore without a specific error code or explanationtyping.Any used as a return type or parameter where a concrete type is knownAction: Tighten the upstream type or add a runtime guard. If runtime-guarded, document as acceptable.
Optional fields that are always present after construction, or mutually exclusive optional fields that should be a
tagged union.
Signals:
or default / if x is None pattern in every consumerOptional throughoutfield(default=None) that are always set before useAction: Make required at construction boundary. Replace mutually exclusive optionals with a tagged union (Literal discriminant + dataclass variants).
is not None checks and or default patterns that compensate for a loose upstream type rather than handling genuine
absence.
Signals:
is not None on a value that is always present given the current contextor default_value applying a default that was already resolved at the entry pointor / if x is None to handle cases that should be separate functionsAction: Tighten the upstream type so the value is guaranteed present. Move defaults to construction boundaries.
Unions with redundant discriminants, missing exhaustiveness checks, or fields that leak across variants.
Signals:
kind vs a boolean flag)match/if-elif over discriminant without a final else: raise/assert_never() for exhaustivenessisinstance/matchAction: Eliminate redundant discriminants. Add exhaustiveness guards (assert_never() from typing). Separate
variant-specific fields into distinct dataclasses.
Guards, assertions, and validations that could be compile-time guarantees.
Signals:
if node.config is not None where config should be guaranteed by the discriminantNewType candidates: IDs, units, validated strings used as plain strNonEmpty[T] type or Annotated[list, MinLen(1)] would eliminate@dataclass(frozen=True) would enforcebool instead of using TypeGuard[T] to narrow the caller's scopetuple/frozenset would enforce immutabilityAction: Promote to type constraint. Use NewType for domain identifiers. Use TypeGuard for runtime guards that
should narrow control flow. Use frozen=True for immutable data. If type complexity would be excessive, keep as
runtime with documentation.
Any, cast(), # type: ignore, and # pyright: ignore — escape hatches that circumvent the type checker.
Boundary with error-hunter: invariant-hunter owns type-system escape hatches (Any, cast(), # type: ignore).
error-hunter owns error suppression patterns (bare except:, catch-and-log-only, silent fallbacks). If the finding
is about bypassing the type checker, it belongs here. If it’s about swallowing exceptions, it belongs in error-hunter.
Signals:
Any as parameter type, return type, or variable annotation without justification# type: ignore without a specific error code (e.g., # type: ignore[attr-defined])cast() without justification comment# pyright: ignore or # mypy: ignore without explanationAction: Fix the underlying type issue. If bypass is necessary, add justification and track as tech debt.
Resolve audit surface. The prompt may specify the scope as:
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.
Record mypy/pyright config: check pyproject.toml, mypy.ini, or pyrightconfig.json for strictness flags
(strict, disallow_any_generics, disallow_untyped_defs, warn_return_any). If key flags are off, note this
prominently.
Scan for patterns:
EXCLUDE='--glob !**/*_test.py --glob !**/test_*.py --glob !**/tests/** --glob !**/venv/** --glob !**/.venv/**'
rg 'cast\(' --type py $EXCLUDE # cast() usage
rg '# type: ignore' --type py $EXCLUDE # type ignores
rg ':\s*Any\b|-> Any\b' --type py $EXCLUDE # Any usage
rg '# (pyright|mypy): ignore' --type py $EXCLUDE # tool-specific ignores
rg 'Optional\[' --type py $EXCLUDE # Optional usage
rg 'is not None|is None' --type py $EXCLUDE # None checks
rg 'assert_never\|TypeGuard' --type py $EXCLUDE # modern typing adoption
rg 'NewType\(' --type py $EXCLUDE # NewType usage
Produce counts by category, grouped by module/layer.
For each tagged union (Literal discriminant, dataclass hierarchy, or class hierarchy):
isinstance/match without cast? List bypass sites.match/if-elif chains have assert_never() or else: raise defaults?Boundary with type-hunter: If the type definition should not be Optional (the field is always present), that’s
a type-hunter finding. If the type is correctly non-Optional but downstream code still does is not None checks,
that’s an invariant-hunter finding.
For each Optional field in core types: Is absence meaningful, or always present after construction?
For each is not None / or default in non-boundary code: Is the property guaranteed present in this context?
Classify each as: tighten type / move default to boundary / acceptable (see Canonical Exceptions).
For each runtime guard/assertion, classify:
For each except block: classify as Remove (no recovery) / Keep (defined boundary) / Move (too broad). For each type bypass: verify justification, scoping, and tech debt tracking.
Save as YYYY-MM-DD-invariant-hunter-audit-{$LLM-name}.md in the project's docs folder (or project root if no docs
folder exists).
# Invariant Hunter Audit — {date}
## Scope
- Surface: {diff / path / codebase}
- Files: {count or list}
- Exclusions: {list}
## Type Checker Context
- Tool: {mypy / pyright / both}
- Config: {path}
- `strict`: {on/off}, `disallow_any_generics`: {on/off}
- `disallow_untyped_defs`: {on/off}, `warn_return_any`: {on/off}
## Baseline
| Category | Count |
| -------- | ----- |
| `cast()` usage | {n} |
| `# type: ignore` (total) | {n} |
| `Any` usage | {n} |
| Tool-specific ignores | {n} |
| `Optional` fields in core types | {n} |
| `is not None` in non-boundary code | {n} |
## Tagged Unions
### {UnionName}
- Discriminant: `{field}`
- Inference: {pass/fail}
- Redundancy: {none / {field} duplicates {other}}
- Exhaustiveness: {pass/fail}
## Optionality and Defensive Access
| # | Field/Expression | Location | Current | Proposed | Rationale |
| - | ---------------- | -------- | ------- | -------- | --------- |
| 1 | ... | file:line | Optional | required | ... |
## Runtime → Type Promotions
| # | Invariant | Location | Current | Proposed | Complexity |
| - | --------- | -------- | ------- | -------- | ---------- |
| 1 | ... | file:line | runtime guard | type constraint | low/med/high |
## Type-System Bypasses
| # | Location | Pattern | Classification | Action |
| - | -------- | ------- | -------------- | ------ |
| 1 | file:line | `Any` return | Remove | Fix type |
## Ergonomics
- Consumer branch count per variant: ...
- Boilerplate patterns: ...
- Extensibility: ...
## Recommendations (Priority Order)
1. **Must-fix**: {narrowing failures, forced casts, silent fallbacks masking bugs}
2. **Should-fix**: {defaults in wrong layer, always-present optionals}
3. **Consider**: {ergonomic improvements, extensibility prep}
file/path.py:line with the exact code.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.