# 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.

```python tab
# 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)
```

```ts tab
// 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.

```python tab
# 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))
```

```ts tab
// 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 (`reportMissingParameterType` and `noImplicitAny` first), and expand.
- **Strict flags rule out some idioms**: untyped decorators, `**kwargs` forwarding, 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.typed` marker 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 `any` need type information and the docs warn of "performance tradeoffs". Run them in CI, not on every keystroke.
- **Flag bundles drift.** mypy says its `--strict` list may change between releases, and TypeScript says the same of `strict`. 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
```markdown
## 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 `strict` list is checked in standard mode even if it imports a strict module. Check which paths are covered.
- **`# type: ignore` and `@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 on `reportMissingTypeStubs`.
- **Rule names differ by tool.** The full pyright configuration page lists no `reportAny` or `reportExplicitAny` rule (checked 2026-10-30). To reject a written `Any` in Python, use mypy's `--disallow-any-explicit` or ruff `ANN401`.

## References
- Pyright configuration (modes, `strict`, rule defaults): https://raw.githubusercontent.com/microsoft/pyright/main/docs/configuration.md
- mypy command line (`--strict` list, `--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 (`strict` family 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?](https://beta.llmgraph.in/agent-readable/ar-07.md)
- [AR-11: Why does my agent break calls that go through **kwargs?](https://beta.llmgraph.in/agent-readable/ar-11.md)
- [AR-13: Why is Any a blind spot for coding agents?](https://beta.llmgraph.in/agent-readable/ar-13.md)
- [AR-14: How should I type callbacks so agents can follow them?](https://beta.llmgraph.in/agent-readable/ar-14.md)
- [AR-15: How do I make duck typing visible to type checkers?](https://beta.llmgraph.in/agent-readable/ar-15.md)
- [AR-16: Why do dict-shaped objects make agents guess?](https://beta.llmgraph.in/agent-readable/ar-16.md)
- [AR-29: What should a coding agent run before finishing?](https://beta.llmgraph.in/agent-readable/ar-29.md)

Last verified 2026-10-30
