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

```ts tab
// 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 tab
# 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"]))
```

```ts tab
// 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
| Tool | Setting | What it catches | Checked |
|---|---|---|---|
| ruff | `ANN001` missing-type-function-argument, `ANN201` missing-return-type-undocumented-public-function | missing parameter and public return annotations | 2026-10-30 |
| ruff | `ANN401` any-type | `Any` written as an argument or return annotation. Type aliases of `Any` are unsupported and can cause false positives | 2026-10-30 |
| pyright | `reportMissingParameterType` (none in basic and standard, error in strict) | parameters without annotation; `self` and `cls` are exempt | 2026-10-30 |
| pyright | `reportUnknownParameterType`, `reportUnknownArgumentType`, `reportUnknownVariableType`, `reportUnknownMemberType` (same defaults) | values pyright could not infer, which is how inherited `Any` shows up | 2026-10-30 |
| pyright | `reportMissingTypeStubs` (error in strict) | imports with no stub file | 2026-10-30 |
| mypy | `--disallow-untyped-defs`, `--disallow-incomplete-defs` (in `--strict`) | functions with no or partial annotations | 2026-10-30 |
| mypy | `--disallow-any-explicit` (not in `--strict`) | `Any` written in annotations | 2026-10-30 |
| mypy | `--disallow-any-unimported` (not in `--strict`) | types that become `Any` because their import was not followed | 2026-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 type | 2026-10-30 |
| tsconfig | `noImplicitAny` (on under `strict`) | any type TypeScript would have inferred as `any` | 2026-10-30 |
| tsconfig | `useUnknownInCatchVariables` (on under `strict`) | `catch (e)` being typed `unknown` instead of `any` | 2026-10-30 |
| typescript-eslint | `no-explicit-any` (in `recommended`), option `fixToUnknown` | `any` written in a type position; the fix can rewrite it to `unknown` | 2026-10-30 |
| typescript-eslint | `no-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 from | 2026-10-30 |
| typescript-eslint | `explicit-module-boundary-types` (not in a preset per its docs page), option `allowArgumentsExplicitlyTypedAsAny` | exported functions and public methods missing parameter or return types | 2026-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)

## Related

- [AR-11: Why does my agent break calls that go through **kwargs?](https://beta.llmgraph.in/agent-readable/ar-11.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-17: Which type checker settings make code easier for agents?](https://beta.llmgraph.in/agent-readable/ar-17.md)

Last verified 2026-10-30
