For AI agentsA Markdown index of these docs is at /llms.txt. Every page is also Markdown: add .md to its address.

Agent Rules / Types and contracts

Why is Any a blind spot for coding agents?

Rule AR-13

Annotate public function parameters and returns. Use Any/any only at a documented boundary.

A value typed Any (Python) or any (TypeScript) switches the checker off for everything done with it. A method call on such a value has no declaration to point at, so no tool can say which class's method runs, and an agent that renames or re-signs that method gets no error from the places that call it. Give public functions real parameter and return types, and keep Any at the one spot where untyped data enters, with a comment saying so.

Why it happens

Python. The typing specification defines Any as "an unknown static type" and says that, because of this, "the static type checker can't check type correctness of operations on expressions typed as Any." It is consistent with every type in both directions: "Any is consistent with every type, and every type is consistent with Any." The typing docs add that "all functions without a return type or parameter types will implicitly default to using Any". So Any arrives in three ways, not one:

  • Written: def f(x: Any).
  • Implied: a parameter with no annotation. Nothing in the file says it is missing a type.
  • Inherited: a value that comes from an untyped library, or from a function that returns Any, carries Any into every variable it is assigned to.

The third way is the quiet one. mypy's docs say that when a package ships neither stubs nor a py.typed marker, "mypy will assume the type of that module is Any, the dynamic type", and that errors on those values can go unnoticed. The typing specification says package authors "MUST add a marker file named py.typed" for their typing to be used, and defines stub packages (foopkg-stubs) for packages that don't ship types. Pyright has its own word for the inferred case: when a symbol's type cannot be inferred, it "sets its type to Unknown, which is a special form of Any", and that lets it warn about undeclared types. Pyright also infers an unannotated parameter from its default value, so def f(retries=3) is int, while a default of None gives Unknown | None.

**Any versus object.** The typing docs separate the two: "Use object to indicate that a value could be any type in a typesafe manner. Use Any to indicate that a value is dynamically typed." Every type is a subtype of object, but object is not a subtype of every type, so the checker still rejects x.send() on an object. A parameter typed object that is then narrowed with isinstance is a valid boundary. A parameter typed Any is not narrowed by anything.

TypeScript. The handbook describes any as the type to use "whenever you don't want a particular value to cause typechecking errors". With it you can "access any properties of it (which will in turn be of type any), call it like a function, assign it to (or from) a value of any type". The any spreads: a property read from an any value is any too. When you leave a type off and TypeScript can't infer one, "the compiler will typically default to any", and noImplicitAny turns that into an error.

TypeScript 3.0 added unknown as the safe counterpart. "Anything is assignable to unknown, but unknown isn't assignable to anything but itself and any without a type assertion or a control flow based narrowing", and "no operations are permitted on an unknown without first asserting or narrowing". unknown is the TypeScript equivalent of Python's object here.

Gradual typing is why this is allowed. Both languages let you add types a function at a time. The specification's gradual guarantee says removing annotations "should not result in additional static type errors", which is why an untyped corner never fails the build. It is also why the checker reports nothing about the calls made from it.

Bad and good

An invoice service prices lines. The Line model is typed in one module and consumed through Any in another.

python# bad: pricing.py from typing import Any def price_line(line: Any, discounts: Any) -> Any: base = line.unit_price * line.qty return base - discounts.get(line.sku, 0) def invoice_total(lines, discounts): # no annotations: implicit Any return sum(price_line(l, discounts) for l in lines)
typescript// bad: pricing.ts export function priceLine(line: any, discounts: any): any { const base = line.unitPrice * line.qty; return base - (discounts[line.sku] ?? 0); } export function invoiceTotal(lines, discounts) { // implicit any under a loose config return lines.reduce((sum, l) => sum + priceLine(l, discounts), 0); }

Now an agent changes Line.unit_price to Line.unit_cents (an integer) and updates the Line class and its tests. The checker has nothing to say about line.unit_price in price_line, because line is Any. The old name stays, fails at runtime on the first priced line, and the arithmetic downstream is typed Any as well.

python# good: pricing.py from dataclasses import dataclass from typing import Mapping @dataclass(frozen=True) class Line: sku: str unit_cents: int qty: int def price_line(line: Line, discounts: Mapping[str, int]) -> int: return line.unit_cents * line.qty - discounts.get(line.sku, 0) def invoice_total(lines: list[Line], discounts: Mapping[str, int]) -> int: return sum(price_line(l, discounts) for l in lines) # boundary: the only place untyped data enters. Everything past this line is typed. def load_line(raw: object) -> Line: if not isinstance(raw, dict): raise ValueError("line must be an object") return Line(sku=str(raw["sku"]), unit_cents=int(raw["unit_cents"]), qty=int(raw["qty"]))
typescript// good: pricing.ts export interface Line { sku: string; unitCents: number; qty: number; } export function priceLine(line: Line, discounts: Record<string, number>): number { return line.unitCents * line.qty - (discounts[line.sku] ?? 0); } export function invoiceTotal(lines: Line[], discounts: Record<string, number>): number { return lines.reduce((sum, l) => sum + priceLine(l, discounts), 0); } // boundary: the only place untyped data enters. Everything past this line is typed. export function loadLine(raw: unknown): Line { if (typeof raw !== "object" || raw === null) throw new Error("line must be an object"); const r = raw as Record<string, unknown>; return { sku: String(r.sku), unitCents: Number(r.unitCents), qty: Number(r.qty) }; }

Now the same rename shows up as an error on every use of the old name, and a search for unit_cents or unitCents finds the real readers. The boundary function does the one job Any was papering over, which is deciding what the incoming data is. See AR-16 for what to put in the type when the data is dict-shaped.

Trade-offs and when Any is acceptable

  • **Any costs nothing to write.** On a prototype, a script or a spike, it is the right call, and gradual typing exists so you can make it. The rule is about code that other code depends on.
  • Real boundaries exist. Data from JSON, a database driver, an untyped library or a plugin can't be known statically. Take it as object or unknown, narrow it once, and return a typed value. That is more lines than Any, and each of them is a check the runtime was going to need.
  • Wide types are honest. Mapping[str, object] or Record<string, unknown> for a bag of data says what is true. dict[str, Any] says nothing, and every value read from it is Any (AR-16).
  • A typed signature is a promise the body has to keep. Annotating a public function with a precise type can surface errors in its callers on the first run. Roll it out by package (AR-17).
  • Tests and generated glue can use Any where the shapes are throwaway. Keep them off the paths agents change.

Enforce it

ToolSettingWhat it catchesChecked
ruffANN001 missing-type-function-argument, ANN201 missing-return-type-undocumented-public-functionmissing parameter and public return annotations2026-10-30
ruffANN401 any-typeAny written as an argument or return annotation. Type aliases of Any are unsupported and can cause false positives2026-10-30
pyrightreportMissingParameterType (none in basic and standard, error in strict)parameters without annotation; self and cls are exempt2026-10-30
pyrightreportUnknownParameterType, reportUnknownArgumentType, reportUnknownVariableType, reportUnknownMemberType (same defaults)values pyright could not infer, which is how inherited Any shows up2026-10-30
pyrightreportMissingTypeStubs (error in strict)imports with no stub file2026-10-30
mypy--disallow-untyped-defs, --disallow-incomplete-defs (in --strict)functions with no or partial annotations2026-10-30
mypy--disallow-any-explicit (not in --strict)Any written in annotations2026-10-30
mypy--disallow-any-unimported (not in --strict)types that become Any because their import was not followed2026-10-30
mypy--disallow-any-generics, --warn-return-any (in --strict)bare list, and returning an Any value from a function with a real return type2026-10-30
tsconfignoImplicitAny (on under strict)any type TypeScript would have inferred as any2026-10-30
tsconfiguseUnknownInCatchVariables (on under strict)catch (e) being typed unknown instead of any2026-10-30
typescript-eslintno-explicit-any (in recommended), option fixToUnknownany written in a type position; the fix can rewrite it to unknown2026-10-30
typescript-eslintno-unsafe-argument, no-unsafe-assignment, no-unsafe-return, no-unsafe-call, no-unsafe-member-access (in recommended-type-checked, need type information)an any value passed, assigned, returned, called or read from2026-10-30
typescript-eslintexplicit-module-boundary-types (not in a preset per its docs page), option allowArgumentsExplicitlyTypedAsAnyexported functions and public methods missing parameter or return types2026-10-30

Pyright's reportUnknown* family catches the inherited kind that ANN401 and --disallow-any-explicit can't see, because no Any is written anywhere. Turn those on for the paths agents edit (AR-17).

Rule block for AGENTS.md / CLAUDE.md

markdown## Types at boundaries - Annotate every parameter and return type on public functions. No implicit Any. - Don't write `Any` or `any` to make an error go away. Use a precise type, `object`/`unknown`, or a Protocol/interface. - Untyped data (JSON, DB rows, untyped libraries) enters through one function that takes `object`/`unknown`, narrows it, and returns a typed value. Mark that function with a comment starting "boundary:". - If you must write Any, add a comment on the same line saying why and what the real type is. - After changing a function with Any in its signature, search for its callers by hand: the type checker can't list them.

Edge cases

  • **cast(Foo, x) and x as Foo** assert a type without checking it. They are Any with extra steps, so treat them as boundaries and comment them.
  • **# type: ignore and @ts-ignore** suppress the error and leave the value's type as it was. A suppressed line is a blind spot the checker has agreed not to mention.
  • **Callable[..., Any] and (...args: any[]) => any** are Any for call signatures (AR-14). The same goes for *args: Any, **kwargs: Any (AR-11).
  • **dict[str, Any] and Record<string, any>** make every value an Any. See AR-16.
  • **An explicit Any is invisible to the pyright reportUnknown* rules.** Pyright's docs describe Unknown as the inferred form of Any. Checked with pyright 1.1.414 in strict mode: an explicit Any on a parameter, a return type and a variable produced no diagnostic, while an unannotated function in the same file was flagged (reportUnknownParameterType, reportUnknownVariableType, reportMissingParameterType).
  • Untyped dependencies. Prefer a stub package or the library's own types. Install the stubs mypy suggests (the types-* packages), write a small stub for what you use, or wrap the library in one typed module and import it only there.
  • **Parameters<any>** is unknown[] and ReturnType<any> is any, per the utility-types page, so a utility type over an any function gives you an any back.
  • **Classes that implements an interface** don't pick up the interface's parameter types. The TypeScript handbook says implements "doesn't change the type of the class or its methods at all", so a method parameter left untyped is still an implicit any.
  • Data classes that wrap untyped fields push the problem inward. Type the fields.

What LLMGRAPH shows

A method called on an Any, unannotated, **kwargs or untyped-import value prints maybe_more ... (open: type unknown); with real types it prints certain.

References

  • Python typing docs, the Any type and Any versus object: https://docs.python.org/3/library/typing.html#the-any-type
  • Typing specification, concepts (gradual types, consistency): https://typing.python.org/en/latest/spec/concepts.html
  • Typing specification, distributing type information (stub files, py.typed, stub-only packages): https://typing.python.org/en/latest/spec/distributing.html
  • Pyright, type inference (Unknown, unannotated parameters): https://raw.githubusercontent.com/microsoft/pyright/main/docs/type-inference.md
  • Pyright configuration: https://raw.githubusercontent.com/microsoft/pyright/main/docs/configuration.md
  • mypy command line (--strict, --disallow-any-*): https://mypy.readthedocs.io/en/stable/command_line.html
  • mypy, running mypy and missing stubs: https://mypy.readthedocs.io/en/stable/running_mypy.html
  • Ruff ANN401: https://docs.astral.sh/ruff/rules/any-type/ · rule list: https://docs.astral.sh/ruff/rules/
  • TypeScript handbook, everyday types (any, noImplicitAny): https://www.typescriptlang.org/docs/handbook/2/everyday-types.html
  • TypeScript 3.0 release notes (unknown): https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-0.html
  • TypeScript handbook, classes (implements): https://www.typescriptlang.org/docs/handbook/2/classes.html
  • TypeScript utility types: https://www.typescriptlang.org/docs/handbook/utility-types.html
  • TSConfig reference: https://www.typescriptlang.org/tsconfig/
  • typescript-eslint: https://typescript-eslint.io/rules/no-explicit-any/ · 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-call/ · https://typescript-eslint.io/rules/no-unsafe-member-access/ · https://typescript-eslint.io/rules/explicit-module-boundary-types/

Changelog

  • 2026-10-30: draft 1.
  • 2026-10-30: docs checks resolved.
  • 2026-10-30: tool checks resolved (pyright 1.1.414, tsc 7.0.2, eslint 9.39.5, ruff 0.17.3, mypy 2.4.1)
  • AR-11 Why does my agent break calls that go through **kwargs?
  • 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-17 Which type checker settings make code easier for agents?

Last verified

For agentsthis page as Markdown/agent-readable/ar-13.mdllms.txt