# Why do __getattr__ proxies confuse coding agents?

> Rule AR-22: Avoid __getattr__ (and JavaScript Proxy) forwarding on domain objects; expose explicit methods.

A proxy class forwards any attribute it doesn't know to another object. `repo.find_by_email(x)` reads like a method call, and there is no `find_by_email` anywhere in the source. An agent that looks for the definition finds nothing, concludes the method is dead or missing, and either deletes the call site or invents an implementation. Write the forwarding methods out, or give the proxy a typed interface, and every name has a definition to land on.

AR-07 covers `getattr` calls with built names. This page covers the other direction: a class that answers for names it doesn't define.

## Why it happens
**Python.** The data model says `__getattr__` is called when the default attribute access fails with an `AttributeError`, and that it isn't called if the attribute is found through the normal mechanism. So the hook sits behind the real lookup, and everything the class doesn't define reaches it as a string. `__getattribute__`, by contrast, is called unconditionally for attribute access on instances. The docs also say implicit special method lookup, such as `len(obj)` reaching `__len__`, generally bypasses `__getattribute__` and so a forwarding hook, which means a proxy has to define those dunder methods by hand anyway.

Two results follow for tools:
- **No definition exists.** The method name is data. A text search finds the call sites and no `def`, and a type checker has no declaration to attach.
- **The type is whatever the hook returns.** Without annotations a hook gives `Any` or an unknown type. Checked with pyright 1.1.414 and mypy 2.4.1: on a class whose `__getattr__` has a declared return type (`int` in the check), both reveal that type for an attribute the class doesn't define, and both flag assigning it to a `str` variable. Neither treated the access as `Any`, so there was no difference between the two tools. mypy's docs say attribute access on an `Any` value gives `Any`, which is the usual outcome.

There is also a hazard in the hook itself: if it reads an attribute that isn't set yet, the read fails, calls the hook again, and can recurse without end. The data model gives the matching advice for `__getattribute__`: call the base class method with the same name to avoid infinite recursion.

**TypeScript and JavaScript.** MDN describes `Proxy` as an object that can intercept and redefine fundamental operations, with a handler whose traps (such as `get`) intercept property access on the target. In the MDN example a `get` trap returns `"world"` for every property read. The compiler sees the type you declared for the proxy (often the target's type, cast in), and the trap's behaviour is invisible to it. Invariants are checked against the target, and a trap that violates them throws a `TypeError` at runtime. The TypeScript handbook describes type assertions (`as`) as compile-time only, with no runtime check, but it doesn't mention `Proxy`, so a cast on a proxy is common practice and not a handbook rule.

## Bad and good
Take a repository that forwards queries to a database client.

```python tab
# bad: the repository answers for names it doesn't define
class UserRepo:
    def __init__(self, db: Database) -> None:
        self._db = db

    def __getattr__(self, name: str):
        return getattr(self._db, name)      # find_by_email? count_active? who knows

repo = UserRepo(db)
user = repo.find_by_email("a@example.com")
```

```ts tab
// bad: a Proxy forwards every property read
const repo = new Proxy({} as UserRepo, {
  get: (_t, prop) => (db as any)[prop],
});

const user = repo.findByEmail("a@example.com");
```

An agent asked to make `find_by_email` case-insensitive searches for the definition, finds it on `Database` in another package, edits it, and changes behaviour for every other user of that client. Or it finds nothing in `UserRepo` and decides the repo has no such method.

```python tab
# good: the surface is written out
class UserRepo:
    def __init__(self, db: Database) -> None:
        self._db = db

    def find_by_email(self, email: str) -> User | None:
        return self._db.find_user(email=email.lower())

    def count_active(self) -> int:
        return self._db.count("users", active=True)
```

```ts tab
// good: the surface is written out
class UserRepo {
  constructor(private db: Database) {}

  findByEmail(email: string): Promise<User | null> {
    return this.db.findUser({ email: email.toLowerCase() });
  }

  countActive(): Promise<number> {
    return this.db.count("users", { active: true });
  }
}
```

Each method has a definition, a signature and callers. Narrowing the surface is a benefit too: the repo exposes what the app needs, and nothing the database client happens to offer.

## Trade-offs and when a proxy is acceptable
- **Forwarding is shorter.** A `__getattr__` that delegates saves a method per forwarded call. The written-out form costs the lines and gives you a place to put validation, logging and renames.
- **Wrapping objects with a very wide API** (a file-like adapter, a client from a library you can't change) is a legitimate use. Forward the few methods your code calls and leave the rest out.
- **Lazy attribute loading** (an ORM field fetched on first access) is a legitimate use. Declare the attribute and its type on the class, and let the hook fill it.
- **Test doubles and mocks** use `__getattr__` or `Proxy` on purpose. Keep them under `tests/`, and see AR-35 for patching.
- **Proxies for observation** (reactivity systems, logging) are a standard JavaScript use of `Proxy`. Keep them behind a function with a declared return type, and don't spread proxied objects through domain code.
- **A `Protocol` or interface** (AR-15) lets you state the wanted surface without writing a forwarding class at all.

## Enforce it
| Tool | Setting | What it catches | Checked |
|---|---|---|---|
| pyright | `reportUnknownMemberType` (none in basic and standard, error in strict) | members whose type is unknown, which is what a hook returns without annotations | 2026-10-30 |
| pyright | `reportAttributeAccessIssue` (error in basic, standard, strict) | access to undeclared attributes, on classes the checker reads as having no hook | 2026-10-30 |
| mypy | `attr-defined` (on by default) | attribute access on a class that doesn't define it | 2026-10-30 |
| tsconfig | `noImplicitAny` (on under `strict`) | untyped proxy handlers and traps | 2026-10-30 |
| typescript-eslint | `no-explicit-any`, `no-unsafe-member-access` | the `as any` casts used to type a proxy | 2026-10-30 |
| eslint | `no-restricted-syntax` with selector `NewExpression[callee.name='Proxy']` | every `new Proxy(...)` in the paths you list; checked with ESLint 9.39.5: `new Proxy(...)` is flagged, and an indirect form through `globalThis.Proxy` is not | 2026-10-30 |

No rule in the pages checked flags a `__getattr__` definition by name. Searching for `def __getattr__` and `new Proxy` in the paths agents edit takes a minute and finds every instance.

## Rule block for AGENTS.md / CLAUDE.md
```markdown
- Don't add `__getattr__` forwarding or `new Proxy(...)` to domain objects. Write the methods out
  with signatures, or define a Protocol / interface for the wanted surface.
- If a class defines `__getattr__`, treat any attribute call on it as unresolved: read the hook and
  its target before editing a method it might serve.
- A hook that stays must be annotated, must raise `AttributeError` for unknown names, and must not
  read attributes that `__init__` sets.
```

## Edge cases
- **Special methods** (`__len__`, `__iter__`, `__enter__`) aren't forwarded by `__getattr__`, because implicit special method lookup bypasses it. A proxy that appears to wrap everything misses the ones you forgot.
- **`__getattr__` at module level** (PEP 562, added in Python 3.7 and described in the data model reference under customizing module attribute access) serves lazy imports and deprecated names. The same visibility cost applies.
- **Dataclass or attrs fields with defaults** aren't affected. The hook is reached only after normal lookup fails.
- **Pickle and copy** may look up attributes on half-built objects. The pickle docs say `__init__()` isn't called when an instance is unpickled and that methods like `__getattr__()` may be called on the instance at that point, and the copy docs say copying uses the same interfaces as pickling. A hook with no guard can then recurse; check `name.startswith("__")` or look the target up through `self.__dict__`.
- **Forwarding in tests** (`MagicMock`) is expected; the rule is about production objects.

## What LLMGRAPH shows
A call through a `__getattr__` proxy prints 'maybe_more ... (open: type unknown)' on the target method. With the methods written out, the call prints certain.

## References
- Python data model, `__getattr__`, `__getattribute__`, special method lookup: https://docs.python.org/3/reference/datamodel.html
- mypy, dynamically typed code (attribute access on `Any`): https://mypy.readthedocs.io/en/stable/dynamic_typing.html
- mypy error codes, `attr-defined`: https://mypy.readthedocs.io/en/stable/error_code_list.html
- Pyright configuration: https://raw.githubusercontent.com/microsoft/pyright/main/docs/configuration.md
- MDN, `Proxy`: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy
- TSConfig reference: https://www.typescriptlang.org/tsconfig/
- typescript-eslint: https://typescript-eslint.io/rules/no-explicit-any/ · https://typescript-eslint.io/rules/no-unsafe-member-access/

## 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)
- 2026-10-30: copy-gate wording fixes

## Related

- [AR-07: Why does my coding agent miss calls made through getattr?](https://beta.llmgraph.in/agent-readable/ar-07.md)
- [AR-15: How do I make duck typing visible to type checkers?](https://beta.llmgraph.in/agent-readable/ar-15.md)
- [AR-19: Why does reassigning methods hide callers from agents?](https://beta.llmgraph.in/agent-readable/ar-19.md)
- [AR-21: Do metaclasses hide code from coding agents?](https://beta.llmgraph.in/agent-readable/ar-21.md)

Last verified 2026-10-30
