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

Agent Rules / Names and imports

Why does my agent not see what importing this module does?

Rule AR-06

Keep import side effects out of modules; registration happens in an explicit setup call.

When importing a file changes global state (fills a registry, opens a connection, patches a class), the line that causes the change is an import, and an import line reads as "I need these names". An agent deleting an import it thinks is unused, or reordering imports, can remove a feature without any name going missing. Make the effect a function call, register_exporters(), that appears in the code and in a search.

Why it happens

Python. The reference describes the basic import statement in two steps: find a module, "loading and initializing it if necessary", then define a name in the current namespace. Initializing means running the module's code, once, and caching the result in sys.modules. Importing a package also runs its __init__.py, so importing exporters.csv runs exporters/__init__.py first. Anything at module level, such as a decorator that records the function in a dict, a call that opens a file, or a class statement with a hook, happens at that point.

Class hooks fire at class definition time too. Python's data model says __init_subclass__ "is called whenever the containing class is subclassed", so a base class can add every subclass to a registry as the subclass statement runs. The registry then contains exactly the subclasses whose modules have been imported by the time you look.

The static view is blind to this. A file that imports exporters.csv and never uses a name from it looks like an unused import. Ruff's F401 reports unused imports, and its fix is to remove them, unless the import is re-exported. A type checker with reportUnusedImport (off in basic and standard, error in strict) says the same.

TypeScript and JavaScript. import "./register-exporters" is a side-effect import: it loads the module and binds nothing. TypeScript treats it differently from a named import in two ways:

  • Without verbatimModuleSyntax, TypeScript drops imports it decides are used only as types. The handbook notes that this makes a difference for modules that have side effects. With verbatimModuleSyntax, imports and exports without a type modifier are left in place, and import { type A } from "mod" becomes import {} from "mod", which still loads the module.
  • By default TypeScript doesn't check that a side-effect import resolves. noUncheckedSideEffectImports makes an unresolvable one an error. It is off by default.

Both runtimes also run module code in an order that follows import order, and what that order is for a given graph is hard to see from one file. In a test on Node v24.19.0, main imported a then c, and a imported b; the output was b, a, c, main, which is depth-first in import order with each module running after its own imports.

Bad and good

python# bad: exporters/csv.py registers itself as a side effect of import from exporters.registry import REGISTRY def export_csv(rows): ... REGISTRY["csv"] = export_csv # runs when anything imports this module
python# bad: main.py import exporters.csv # looks unused; a linter removes it import exporters.xlsx
python# good: exporters/registry.py from exporters.csv import export_csv from exporters.xlsx import export_xlsx def register_exporters() -> dict: return {"csv": export_csv, "xlsx": export_xlsx}
python# good: main.py from exporters.registry import register_exporters EXPORTERS = register_exporters() # visible call, searchable, used
ts// bad: src/main.ts import "./plugins/csv"; // registers itself; no name is used import "./plugins/xlsx";
ts// good: src/plugins/index.ts import { csvExporter } from "./csv"; import { xlsxExporter } from "./xlsx"; export function registerExporters(registry: Map<string, Exporter>): void { registry.set("csv", csvExporter); registry.set("xlsx", xlsxExporter); }

In the good versions each module exports a plain object or function, and one setup call makes the registry. Removing csv is a visible edit in one list, and a test can build a registry in a few lines without importing the whole app.

Trade-offs and when an import side effect is acceptable

  • Import-time registration is convenient. Adding a plugin means adding a file. The explicit version needs a line in a list. That line is the price of making the list readable.
  • Polyfills and global CSS or style imports (import "./styles.css") are side effects by design. Keep them at the entry file, and in TypeScript decide on noUncheckedSideEffectImports knowing a CSS import will need a wildcard module declaration (the option's docs show declare module "*.css" {}).
  • Loggers and constants with no effect beyond creating an object are fine. The rule is about changing something other code later reads.
  • Cheap, idempotent effects (compiling a regular expression, building a lookup table at module level) are fine. Moving them behind a function is only worth it if they can fail or take time.
  • **__init_subclass__ registries** are legitimate for a framework. If you keep one, keep the list of modules that must be imported in one explicit place, and test that the registry holds what you expect.
  • Lazy imports. Python's reference says that from Python 3.15 an import preceded by lazy creates a proxy and loads the module on first use. That moves the point at which side effects run, so a registry filled by import becomes dependent on first use. Avoid combining the two.

Enforce it

ToolSettingWhat it catchesChecked
ruffF401 unused-importside-effect imports that are used for nothing the checker can see (a signal to replace them with a call, or to mark them deliberately)2026-10-30
ruffPLC0415 import-outside-top-levelimports hidden inside functions and classes2026-10-30
pyrightreportUnusedImport (none in basic and standard, error in strict)imports with no reference in the file2026-10-30
tscnoUncheckedSideEffectImports (default off)a bare import "x" whose source file can't be found; the option arrived in TypeScript 5.6 (TypeScript 5.6.2 is the earliest version verified; TypeScript 5.5.4 gives TS5023, unknown compiler option). The unresolved import is reported as TS2307 on TypeScript 5.6 and TypeScript 5.7, and as TS2882 on TypeScript 7.0.22026-10-30
tscverbatimModuleSyntaxkeeps plain imports in the output, so a side-effect import isn't silently dropped2026-10-30
typescript-eslintno-import-type-side-effectsimport { type A } from "mod", which leaves a stray import "mod" after compilation; the fix is import type { A } from "mod"2026-10-30
eslintno-restricted-syntax with selector ImportDeclaration[specifiers.length=0]every bare import "x" in code you choose. The length form is an ESTree property of the specifiers array; ESLint 9.39.5 flagged a bare import "./polyfill.js" and also import {} from "./b.js", which has zero specifiers too, and did not flag a named import2026-10-30

No tool in the docs checked detects import-time registration directly. Search for it: rg -n "^[A-Z_]+\[.+\]\s*=" src finds module-level dictionary writes, and rg -n "__init_subclass__|@register" src finds the common hooks.

In pyproject.toml:

toml[tool.ruff.lint] extend-select = ["F401", "PLC0415"]

In tsconfig.json:

json{ "compilerOptions": { "verbatimModuleSyntax": true, "noUncheckedSideEffectImports": true } }

Rule block for AGENTS.md / CLAUDE.md

markdown- Importing a module must not change global state. Don't fill registries, patch classes, open connections or start threads at module level. - Register things in an explicit function (`register_*()`) called from the entry point. - Never delete or reorder an `import x` line that binds nothing until you have searched for what it registers. Replace it with the explicit call instead. - Keep style and polyfill imports in the entry file, not in library code.

Edge cases

  • A decorator that registers (@register("csv")) is a side effect of import. The fix is the same: return the function unchanged and register in the setup call, or accept the decorator and list the modules to import in one place.
  • **Package __init__.py** runs on any submodule import, so work done there happens for every user of the package. Keep __init__.py to imports and __all__ (AR-02).
  • Cached modules: a second import doesn't rerun the module body, so a side effect that should happen per test run needs a function.
  • Failed imports are retried on the next import, since the docs say a module that fails to load is removed from sys.modules. A half-run side effect may then run twice.
  • Conditional import plus side effect: the worst combination. AR-05 covers the conditional half.
  • CH-03 is the corpus case: a registry filled through __init_subclass__ and then called by string key.

What LLMGRAPH shows

A function filled into a registry prints no calls to it found in this repo, passed as a value in 1 place: calls through it are not visible and plus 1 dynamic call nearby that could reach anything. An explicit register_exporters() call prints as a certain call; calls through the registry stay open.

References

  • Python Language Reference, "The import statement" (find, load and initialize; lazy imports): https://docs.python.org/3/reference/simple_stmts.html#the-import-statement
  • Python Language Reference, "The import system" (sys.modules cache, package __init__.py execution, failed imports): https://docs.python.org/3/reference/import.html
  • Python Language Reference, data model (__init_subclass__): https://docs.python.org/3/reference/datamodel.html
  • Ruff F401 and PLC0415: https://docs.astral.sh/ruff/rules/unused-import/ · https://docs.astral.sh/ruff/rules/import-outside-top-level/
  • Pyright configuration: https://github.com/microsoft/pyright/blob/main/docs/configuration.md
  • TypeScript noUncheckedSideEffectImports: https://www.typescriptlang.org/tsconfig/noUncheckedSideEffectImports.html
  • TypeScript verbatimModuleSyntax: https://www.typescriptlang.org/tsconfig/verbatimModuleSyntax.html
  • typescript-eslint no-import-type-side-effects: https://typescript-eslint.io/rules/no-import-type-side-effects/
  • ECMAScript spec, module environment setup: https://tc39.es/ecma262/multipage/ecmascript-language-scripts-and-modules.html
  • Node.js ESM docs (bare import for its effect): https://nodejs.org/api/esm.html
  • ESLint selectors: https://eslint.org/docs/latest/extend/selectors

Changelog

  • 2026-10-30: draft 1.
  • 2026-10-30: tool checks resolved (tsc 7.0.2, eslint 9.39.5, ruff 0.17.3, mypy 2.4.1, pyright 1.1.414)
  • AR-05 Why do fallback imports make agents edit the wrong code?
  • AR-02 Do barrel files hide calls from coding agents?
  • CH-03 Why did my plugin registry lose a format after an edit?

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-06.mdllms.txt