Agent Rules / Types and contracts
Which type checker settings make code easier for agents?
Rule AR-17
Turn on strict type checking in the paths agents edit (pyright strict / mypy strict / tsconfig strict).
The default settings of all three checkers accept code whose types are missing, and silence is the result: a call on an untyped value passes, so an edit that breaks it passes too. Strict modes make the checker refuse unknown types, so an edit that breaks a caller stops the build. You don't need to turn strict on for the whole repository. Turn it on for the packages agents change, and widen it as they get clean.
Why it happens
Each tool has a lenient default and a documented way to raise it.
Pyright. The default typeCheckingMode is standard; the modes are off, basic, standard and strict. The strict setting takes "paths of directories or files that should use strict analysis", and "this is the same as manually adding a # pyright: strict comment". "In strict mode, most type-checking rules are enabled." The rules that matter for this rulebook are off in basic and standard and errors in strict:
| Pyright rule | basic | standard | strict |
|---|---|---|---|
reportMissingParameterType | none | none | error |
reportUnknownParameterType | none | none | error |
reportUnknownArgumentType | none | none | error |
reportUnknownVariableType | none | none | error |
reportUnknownMemberType | none | none | error |
reportUnknownLambdaType | none | none | error |
reportMissingTypeArgument | none | none | error |
reportMissingTypeStubs | none | none | error |
reportUntypedFunctionDecorator | none | none | error |
reportUntypedBaseClass | none | none | error |
reportMatchNotExhaustive | none | none | error |
reportPrivateUsage | none | none | error |
reportIncompatibleMethodOverride | none | error | error |
strictListInference, strictDictionaryInference | false | false | true |
In strict mode, per-rule overrides "may only increase the strictness", so a file with # pyright: strict can't switch these rules back off.
mypy. --strict is a bundle. The docs list what it turns on in this release and say the list may change between releases: --disallow-any-generics, --disallow-subclassing-any, --disallow-untyped-calls, --disallow-untyped-defs, --disallow-incomplete-defs, --check-untyped-defs, --disallow-untyped-decorators, --warn-redundant-casts, --warn-unused-ignores, --warn-return-any, --no-implicit-reexport, --strict-equality, --extra-checks. The flags that most affect hidden calls are not in the bundle: --disallow-any-explicit (rejects Any written in annotations), --disallow-any-unimported (types from unfollowed imports become Any), --disallow-any-expr and --disallow-any-decorated. Per-module sections ([mypy-PATTERN]) apply flags "only to modules whose name matches" a pattern, which is how you adopt strict flags package by package.
TypeScript. The strict flag "is equivalent to enabling all of the strict mode family options": alwaysStrict, noImplicitAny, noImplicitThis, strictNullChecks, strictBindCallApply, strictBuiltinIteratorReturn, strictFunctionTypes, strictPropertyInitialization and useUnknownInCatchVariables. The docs say future versions may add more under this flag. Four checks that matter for finding callers are outside strict: noUncheckedIndexedAccess, noPropertyAccessFromIndexSignature, exactOptionalPropertyTypes and noImplicitOverride.
Lint-level checks. The checker covers declared types. The any values that arrive from untyped code are caught by typescript-eslint rules that need type information. The strict-type-checked config "contains all of recommended, recommended-type-checked, and strict" plus further type-aware rules. The typescript-eslint docs call it "not considered stable under semver", so pin the version.
Bad and good
A repository checks types, but only with the defaults.
# bad: pyrightconfig.json (defaults: standard mode), billing/rates.py
def apply_rate(amount, rate=None):
return amount * (rate or DEFAULT_RATE)
def invoice_total(lines):
return sum(apply_rate(l.amount, l.rate) for l in lines)// bad: tsconfig.json has no "strict", pricing/rates.ts
export function applyRate(amount, rate) {
return amount * (rate ?? DEFAULT_RATE);
}
export function invoiceTotal(lines) {
return lines.reduce((sum, l) => sum + applyRate(l.amount, l.rate), 0);
}Both files check clean. An agent changes applyRate to take the rate as a required second argument in cents. Every call site passes through unknown types, so the checker has nothing to compare, and the mismatch surfaces when a test or a user hits the line.
# good: pyrightconfig.json
# {
# "typeCheckingMode": "standard",
# "strict": ["billing", "orders"]
# }
# (pyproject.toml works too, under [tool.pyright])
from decimal import Decimal
def apply_rate(amount: Decimal, rate: Decimal | None = None) -> Decimal:
return amount * (rate if rate is not None else DEFAULT_RATE)
def invoice_total(lines: list[Line]) -> Decimal:
return sum((apply_rate(l.amount, l.rate) for l in lines), Decimal(0))// good: tsconfig.json
// { "compilerOptions": { "strict": true, "noUncheckedIndexedAccess": true } }
export function applyRate(amount: number, rate: number | undefined): number {
return amount * (rate ?? DEFAULT_RATE);
}
export function invoiceTotal(lines: Line[]): number {
return lines.reduce((sum, l) => sum + applyRate(l.amount, l.rate), 0);
}With strict on, the first file fails to check until the parameters have types, and after that the change to applyRate is an error at each call that doesn't match. The pyright config above keeps the rest of the repository on standard while billing and orders are held to strict. For mypy, the same staged rollout is a [mypy-billing.*] section with disallow_untyped_defs = True and the other flags you want. Config names use snake case in the config file.
Trade-offs and when strict is the wrong choice
- Strict pays back on code that changes often. A package that agents edit weekly gets the most from it. A frozen vendored module gets nothing.
- The first week is a wall of errors. On an untyped codebase, strict mode reports thousands of unknowns. Start with one package, or one rule (
reportMissingParameterTypeandnoImplicitAnyfirst), and expand. - Strict flags rule out some idioms: untyped decorators,
**kwargsforwarding, dict-shaped records. Those are the patterns this rulebook already asks you to replace (AR-11, AR-12, AR-16). - Third-party code decides your count. Libraries without stubs or a
py.typedmarker produce unknowns in your strict files (AR-13). Install stubs, write a small stub, or wrap the library in one module. - Type-aware linting costs time. The typescript-eslint rules that look for
anyneed type information and the docs warn of "performance tradeoffs". Run them in CI, not on every keystroke. - Flag bundles drift. mypy says its
--strictlist may change between releases, and TypeScript says the same ofstrict. Pin tool versions, and read the changelog when you upgrade.
Enforce it
| Tool | Setting | What it does | Checked |
|---|---|---|---|
| pyright | strict (paths) in pyrightconfig.json or [tool.pyright]; or # pyright: strict at the top of a file | applies the strict rule set to listed paths or one file | 2026-10-30 |
| pyright | typeCheckingMode (off, basic, standard, strict; default standard) | repository-wide default | 2026-10-30 |
| mypy | --strict, or strict = True in the config | the bundle listed above; global, with per-module flags in [mypy-PATTERN] sections | 2026-10-30 |
| mypy | --disallow-any-explicit, --disallow-any-unimported (not in --strict) | Any written or imported in | 2026-10-30 |
| tsconfig | "strict": true | the nine options above | 2026-10-30 |
| tsconfig | noUncheckedIndexedAccess, noPropertyAccessFromIndexSignature | not part of strict; adds undefined to undeclared fields, and forces bracket access on index signatures | 2026-10-30 |
| typescript-eslint | strict-type-checked config; or recommended-type-checked | no-unsafe-* rules and other type-aware rules (need parserOptions for type information) | 2026-10-30 |
| ruff | ANN rules (ANN001, ANN201, ANN401) | missing annotations and Any annotations, without running a type checker | 2026-10-30 |
Rule block for AGENTS.md / CLAUDE.md
## Type checking
- Strict mode applies to: `billing/`, `orders/` (Python, pyright strict) and `src/` (TypeScript, `strict: true`). Don't loosen it, and don't add `# type: ignore` or `@ts-ignore` without a comment saying why.
- Before saying a change is done, run the type checker over the changed package and fix every new error. Don't count "no new errors in changed files" as done if callers elsewhere now fail.
- New files in a strict package start strict. New packages ask for the strictest setting the repo uses.
- If strict mode reports unknown types from a third-party import, add or install stubs, or wrap the import in one typed module.Edge cases
- Strict applies where the config says it does. A file outside the
strictlist is checked in standard mode even if it imports a strict module. Check which paths are covered. - **
# type: ignoreand@ts-ignore** survive strict mode. mypy's--warn-unused-ignores(in--strict) flags ignores that no longer suppress anything. - Inference does most of the work. The TypeScript handbook says you "usually don't need a return type annotation" because TypeScript infers it. Strict mode asks for types where inference can't supply them, so it doesn't mean annotating every line.
- **
useLibraryCodeForTypes** in pyright defaults to true, so pyright reads library source to infer types when stubs are missing. This can hide the missing stub until you turn onreportMissingTypeStubs. - Rule names differ by tool. The full pyright configuration page lists no
reportAnyorreportExplicitAnyrule (checked 2026-10-30). To reject a writtenAnyin Python, use mypy's--disallow-any-explicitor ruffANN401.
References
- Pyright configuration (modes,
strict, rule defaults): https://raw.githubusercontent.com/microsoft/pyright/main/docs/configuration.md - mypy command line (
--strictlist,--disallow-any-*): https://mypy.readthedocs.io/en/stable/command_line.html - mypy configuration file (per-module sections): https://mypy.readthedocs.io/en/stable/config_file.html
- TSConfig reference (
strictfamily and the options outside it): https://www.typescriptlang.org/tsconfig/ - TypeScript handbook, everyday types (inference): https://www.typescriptlang.org/docs/handbook/2/everyday-types.html
- typescript-eslint configs: https://typescript-eslint.io/users/configs/
- typescript-eslint rules: https://typescript-eslint.io/rules/no-unsafe-call/ · https://typescript-eslint.io/rules/no-unsafe-argument/ · https://typescript-eslint.io/rules/no-unsafe-assignment/ · https://typescript-eslint.io/rules/no-unsafe-return/ · https://typescript-eslint.io/rules/no-unsafe-member-access/
- Ruff rules (
ANN): https://docs.astral.sh/ruff/rules/any-type/
Changelog
- 2026-10-30: draft 1.
- 2026-10-30: docs checks resolved.
Related
- AR-07 Why does my coding agent miss calls made through getattr?
- AR-11 Why does my agent break calls that go through **kwargs?
- AR-13 Why is Any a blind spot for coding agents?
- AR-14 How should I type callbacks so agents can follow them?
- AR-15 How do I make duck typing visible to type checkers?
- AR-16 Why do dict-shaped objects make agents guess?
- AR-29 What should a coding agent run before finishing?
Last verified