Agent Rules / Structure and state
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
Anyor an unknown type. Checked with pyright 1.1.414 and mypy 2.4.1: on a class whose__getattr__has a declared return type (intin the check), both reveal that type for an attribute the class doesn't define, and both flag assigning it to astrvariable. Neither treated the access asAny, so there was no difference between the two tools. mypy's docs say attribute access on anAnyvalue givesAny, 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.
# 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")// 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.
# 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)// 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__orProxyon purpose. Keep them undertests/, 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
Protocolor 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
- 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; checkname.startswith("__")or look the target up throughself.__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?
- AR-15 How do I make duck typing visible to type checkers?
- AR-19 Why does reassigning methods hide callers from agents?
- AR-21 Do metaclasses hide code from coding agents?
Last verified