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

Agent Rules / Structure and state

Do metaclasses hide code from coding agents?

Rule AR-21

Keep metaclass and __init_subclass__ generation out of edit paths; when you use it, document what it generates.

A metaclass or an __init_subclass__ hook runs when a class is created, and it can add methods, rename attributes, register the class or rewrite the class body. The result is a class whose members don't all appear in its source. An agent that reads the class sees the declared half and edits it without hesitation. Generate members in a visible place, or type what is generated, and the class source becomes the whole story again.

AR-09 and AR-20 already cover registries filled this way. This page covers the class-shape question: what does the class contain, and where is that written down.

Why it happens

Python. The data model spells out class creation in five steps: resolve base-class entries, determine the metaclass, prepare the namespace, execute the body, create the class object. By default a class is built with type(name, bases, namespace). A metaclass= keyword, or inheriting one from a base, swaps that step for your own code. The metaclass can also define __prepare__ to supply the namespace the body runs in.

__init_subclass__ is a lighter hook. The docs say it's called whenever the containing class is subclassed, with the new subclass as cls, and keyword arguments from the class statement are passed to it. Its default does nothing. __set_name__ is a third: it's called on each attribute that defines it, at class creation, with the owner and the attribute name.

All three are legitimate and documented. They share one property for tooling: what the class ends up with depends on code that runs at class creation, in a file other than the class.

Static checkers can follow some of it. mypy's docs say it supports attribute lookup on the metaclass and has special understanding of ABCMeta and EnumMeta. They also say it doesn't understand dynamically computed metaclasses such as metaclass=f(), doesn't and cannot understand arbitrary metaclass code, and only recognises subclasses of type as potential metaclasses. The typing specification offers dataclass_transform for the common case: it tells a checker that a decorated function, class or metaclass performs runtime generation with dataclass-like semantics, so the checker can model the generated __init__. It declares one generated shape, and nothing else.

TypeScript and JavaScript. There are no metaclasses, and the same pattern shows up as class decorators, base classes that define properties in a constructor, and mixin functions that take a class and return a larger one. The TypeScript handbook describes mixins as combining simpler partial classes, and shows the basic constructor type as new (...args: any[]) => {}. The handbook notes the plain form has no underlying knowledge of the class passed in, and offers a generic constructor constraint to fix it. A constrained mixin is checked. An unconstrained one gives the compiler nothing to follow.

Bad and good

Take a model base class that builds field accessors from annotations.

python# bad: the metaclass writes methods that no file contains class ModelMeta(type): def __new__(mcls, name, bases, ns): for field in ns.get("__annotations__", {}): ns[f"get_{field}"] = lambda self, f=field: self.__dict__[f] return super().__new__(mcls, name, bases, ns) class Invoice(metaclass=ModelMeta): total: int currency: str invoice.get_total() # defined nowhere in the source
typescript// bad: a decorator adds methods the class body never declares function withGetters<T extends new (...a: any[]) => object>(base: T) { return class extends base { constructor(...a: any[]) { super(...a); for (const k of Object.keys(this)) { (this as any)[`get${k[0].toUpperCase()}${k.slice(1)}`] = () => (this as any)[k]; } } }; } @withGetters class Invoice { total = 0; currency = "INR"; } new Invoice().getTotal(); // not declared, not type-checked

An agent asked to rename total to amount renames the field. get_total stops existing, and the failure appears at a call the agent never saw, in a file it never read.

python# good: members are written out; the base class only checks them from dataclasses import dataclass @dataclass class Invoice: total: int currency: str def get_total(self) -> int: return self.total
typescript// good: members are written out class Invoice { constructor(public total: number, public currency: string) {} getTotal(): number { return this.total; } }

When the generated shape really is the point, declare it. @dataclass and @dataclass_transform are the supported way to say "this decorator or base class writes __init__ from the annotations", and a checker that knows the decorator models it. For an __init_subclass__ hook that registers or validates, keep the hook short, name what it does in the base class docstring, and put the expected keyword arguments in its signature.

Trade-offs and when a metaclass is acceptable

  • Generated members save lines. The dynamic form writes accessors once for forty fields. The explicit form costs forty short methods, or a dataclass that does it for you in a way the tools understand.
  • ORMs, enums and ABCs use metaclasses well, and checkers have built-in knowledge of the common ones. Using a documented framework base class is a different case from writing your own metaclass.
  • **__init_subclass__ over a metaclass.** When you need a hook at subclass creation, prefer it: it's one method, it composes with other bases, and it avoids metaclass conflicts, which Python reports as a TypeError when bases have incompatible metaclasses.
  • Validation-only hooks (raise if a subclass forgets an attribute) generate nothing. They're safe for tools, and they help agents by failing early.
  • Hooks that register the class are import-time writes to a registry. Treat them with AR-20 and AR-06.

Enforce it

ToolSettingWhat it catchesChecked
mypymetaclass error code (a metaclass conflict between bases)incompatible metaclasses across bases2026-10-30
pyrightreportUnknownMemberType (none in basic and standard, error in strict)member access whose type is unknown, which generated attributes become2026-10-30
pyrightreportAttributeAccessIssue (error in basic, standard, strict)access to attributes the class doesn't declare2026-10-30
tsconfignoImplicitAny, noImplicitThis (on under strict)untyped mixin and decorator internals2026-10-30
typescript-eslintno-explicit-any, no-unsafe-member-accessthe any casts a decorator or mixin needs to add members2026-10-30

No rule in the pages checked flags a metaclass as such. The lever is attribute access on a generated name: a strict checker reports it, and that report is the signal to write the member out or declare it with dataclass_transform.

Rule block for AGENTS.md / CLAUDE.md

markdown- Don't add a metaclass, or an `__init_subclass__` that creates methods or attributes, in code agents edit. Write members out, use `@dataclass`, or declare the shape with `typing.dataclass_transform`. - If a class has a metaclass or `__init_subclass__` hook, read it before editing the class, and list what it generates in the base class docstring. - Before renaming a field on a class with a metaclass, search for the field name inside f-strings and strings.

Edge cases

  • **Enum and ABC** have metaclasses. Checkers model them, and the rule doesn't apply to using them.
  • **__set_name__ descriptors** (ORM fields, validators) are called with the attribute's owner and name at class creation. They're visible in the class body, so the cost is low.
  • Class decorators that return a new class (@add_repr) change what the name refers to. Keep their return type honest (see AR-12).
  • **type(name, bases, ns) called directly** to build classes from data is metaclass territory and has the same cost.
  • **TypeScript declaration merging and declare module** can add members to a type from another file. The handbook says the declarations in an augmentation are merged as if they were declared in the same file as the original, with two limits: an augmentation can't declare new top-level declarations, and default exports can't be augmented.

What LLMGRAPH shows

A method that a metaclass generates has no definition for LLMGRAPH to name: asking for it prints 'no function, method, class or component named ...', and the call is counted as 'dynamic call 1'. Written out, the call prints certain, linked by the type checker.

References

  • Python data model, customizing class creation, __init_subclass__, __set_name__, __prepare__: https://docs.python.org/3/reference/datamodel.html
  • mypy, metaclasses: https://mypy.readthedocs.io/en/stable/metaclasses.html
  • mypy error codes: https://mypy.readthedocs.io/en/stable/error_code_list.html
  • Typing specification, dataclass transforms: https://typing.python.org/en/latest/spec/dataclasses.html
  • Pyright configuration: https://raw.githubusercontent.com/microsoft/pyright/main/docs/configuration.md
  • TypeScript handbook, mixins: https://www.typescriptlang.org/docs/handbook/mixins.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-member-access/

Changelog

  • 2026-10-30: draft 1.
  • 2026-10-30: docs checks resolved.
  • 2026-10-30: copy-gate wording fixes
  • AR-06 Why does my agent not see what importing this module does?
  • AR-09 How should plugin registries be written for coding agents?
  • AR-20 Where should a registry live so coding agents can find it?
  • AR-22 Why do __getattr__ proxies confuse coding agents?

Chimeras that use this rule

  • CH-03 Why did my plugin registry lose a format after an edit?

Last verified

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