Agent Rules / Structure and state
Why does reassigning methods hide callers from agents?
Rule AR-19
Keep a function's callers findable: don't assign functions onto objects at runtime (obj.method = other).
A call to client.send(req) reads as a call to the send defined in the class. If some other file has run client.send = with_retry(client.send), the call goes somewhere else, and the class body says nothing about it. An agent that edits Client.send fixes the wrong function or misses the wrapper's assumptions. Put the replacement in the class hierarchy or in an explicit wrapper type, and the call resolves to a definition you can read.
Why it happens
Python. The data model says attribute assignments on an instance update the instance's dictionary, never the class's dictionary, unless the class defines __setattr__. So client.send = other puts a function in client.__dict__. The next client.send(...) finds that entry before it reaches the class. Two objects of the same class can now answer the same call differently, and the difference exists only while the program runs. The same holds for class-level assignment (Client.send = other), which changes every instance from a file that has nothing to do with Client.
Static checkers see this from the outside. mypy reports assignment to a method on a class or instance by default under the method-assign code, because Python's static types can't tell bound from unbound callables. Pyright's reportAttributeAccessIssue covers problems with attribute accesses and is an error in basic, standard and strict mode. Pyright's docs on declared types add that an undeclared attribute is an error to access and that an assigned value has to match the declared type. In every case the checker works from the declaration in the class, so the replacement function is not a callee it knows about.
types.MethodType is the documented type of methods of user-defined class instances, and it is the usual way to bind a plain function to one object. The binding still happens at runtime, so the same caveat holds.
TypeScript and JavaScript. Methods are properties, and JavaScript lets any code overwrite a property on an object. TypeScript accepts client.send = withRetry(client.send) when the new function matches the declared type. Checked with tsc 7.0.2 under --strict: the matching case gave no error, and a wrapper returning (msg: number) => void where (msg: string) => void is declared gave TS2322 on the assignment line (line 4 of patch.ts). The declaration of send stays where it was, and the override sits in a different file.
TypeScript 3.1 added a related feature: assigning properties onto a function declaration or a const-declared function in the same scope declares them (readImage.sync = ...). The release notes present this as support for existing JavaScript style and note that ECMAScript exports are often a better way to provide it. That form is visible in the same scope as the function, which is the opposite of patching from a distance.
Bad and good
Take an HTTP client that gains retries.
# bad: patch the instance from another module
# app/setup.py
from app.http import Client
from app.retry import with_retry
client = Client("https://api.example.com")
client.send = with_retry(client.send) # Client.send is no longer what runs// bad: patch the instance from another module
// app/setup.ts
import { Client } from "./http";
import { withRetry } from "./retry";
export const client = new Client("https://api.example.com");
client.send = withRetry(client.send.bind(client)); // Client.send is no longer what runsAn agent asked to add a timeout parameter to Client.send updates the class and its direct callers. The wrapper still calls the old shape, and the failure shows up at the first retried request.
# good: the replacement is a class you can find
# app/http.py
class Client:
def send(self, req: Request) -> Response: ...
class RetryingClient(Client):
def send(self, req: Request) -> Response:
return with_retry(super().send)(req)
# app/setup.py
client = RetryingClient("https://api.example.com")// good: the replacement is a class you can find
// app/http.ts
export interface Sender { send(req: Request): Promise<Response>; }
export class Client implements Sender {
send(req: Request): Promise<Response> { /* ... */ }
}
export class RetryingClient implements Sender {
constructor(private inner: Sender) {}
send(req: Request): Promise<Response> {
return withRetry(() => this.inner.send(req));
}
}
// app/setup.ts
export const client = new RetryingClient(new Client("https://api.example.com"));A search for send now lands on both definitions, the overriding signature is checked against the base, and client has a type that says what it does.
Trade-offs and when assignment is acceptable
- Patching is quick. One line adds behaviour to an object you don't control. A wrapper class costs a few lines more, and it gives every reader the answer to "what runs here".
- Tests patch on purpose. Do it through the fixture or mock API, at the place the name is looked up (AR-18). That keeps the patch undone after the test.
- Third-party objects you can't subclass are the honest exception. Put the patch in one function with a name, a docstring and a single call from your startup code, and leave a comment where the original is used.
- Function-with-properties in TypeScript (
fn.displayName = "x") is declared in the same scope as the function. That's fine. - Decorators replace a function at definition time, in the file that defines it. That's visible at the definition, though the signature can still change (see AR-12).
- Dynamic languages feel natural here. The rule asks for the dynamic step to sit inside a named construct, not for it to disappear.
Enforce it
| Tool | Setting | What it catches | Checked |
|---|---|---|---|
| mypy | error code method-assign (on by default) | assignment to a method on a class or instance | 2026-10-30 |
| pyright | reportAttributeAccessIssue (error in basic, standard, strict) | problems with attribute accesses, including undeclared attributes and values of the wrong declared type | 2026-10-30 |
| ruff | B010 set-attr-with-constant | setattr(obj, "send", f) with a literal name; use plain assignment, then review it | 2026-10-30 |
| eslint | no-extend-native | changes to the prototype of a built-in object | 2026-10-30 |
| eslint | no-restricted-syntax with a selector on assignments to a member of a named object | project-specific patching, for example assignments to a member of client. Checked with ESLint 9.39.5: the selector flags any member of client (client.send and client.retries alike), not only send | 2026-10-30 |
No rule in the pages checked flags obj.method = fn in TypeScript when the types match. A selector for assignments whose left side is a member expression and whose right side is a call to a wrapper is the lever, and it needs project knowledge to be useful.
Rule block for AGENTS.md / CLAUDE.md
- Don't assign functions onto objects or classes at runtime (`obj.method = f`, `Class.method = f`).
Subclass, wrap, or inject a collaborator through the constructor instead.
- If you must patch a third-party object, do it in one named function called from startup,
and list that function in the module docstring.
- Before changing a method's signature, search for assignments to that method's name as well as calls.Edge cases
- **
functools.wrapsandpartialobjects assigned to an attribute** hide the target the same way. The call site names the attribute, and the target was bound elsewhere. - Class-level patching in a plugin (
Client.send = patched) changes every instance. It deserves a name and a line in the registry that owns it (AR-20). - **Mixins that add methods through
setattrin a loop** belong to the same family as AR-07: the method names exist only as strings. - Python modules as namespaces:
module.func = otherrebinds a global for every importer. Tests do this withmonkeypatch, and production code shouldn't. - **JavaScript
Object.assign(target, { method })** andObject.definePropertyare the same operation in different syntax.
What LLMGRAPH shows
When a method is assigned onto an object elsewhere, the callers of the class method print with same-name attribute uses, not checked: 1. The subclass form prints certain on the subclass's method.
References
- Python data model, class instances and attribute assignment: https://docs.python.org/3/reference/datamodel.html
- Python
types.MethodType: https://docs.python.org/3/library/types.html - mypy error codes,
method-assign: https://mypy.readthedocs.io/en/stable/error_code_list.html - Pyright configuration,
reportAttributeAccessIssue: https://raw.githubusercontent.com/microsoft/pyright/main/docs/configuration.md - Pyright, type concepts (declared types, undeclared attributes): https://raw.githubusercontent.com/microsoft/pyright/main/docs/type-concepts-advanced.md
- Ruff B010: https://docs.astral.sh/ruff/rules/set-attr-with-constant/
- TypeScript 3.1 release notes, properties declarations on functions: https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-1.html
- ESLint
no-extend-native: https://eslint.org/docs/latest/rules/no-extend-native
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-07 Why does my coding agent miss calls made through getattr?
- AR-12 Do decorators hide function signatures from coding agents?
- AR-18 Why does monkeypatching break agent edits?
- AR-22 Why do __getattr__ proxies confuse coding agents?
Last verified