# 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
| Tool | Setting | What it catches | Checked |
|---|---|---|---|
| ruff | `F401` unused-import | side-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 |
| ruff | `PLC0415` import-outside-top-level | imports hidden inside functions and classes | 2026-10-30 |
| pyright | `reportUnusedImport` (none in basic and standard, error in strict) | imports with no reference in the file | 2026-10-30 |
| tsc | `noUncheckedSideEffectImports` (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.2 | 2026-10-30 |
| tsc | `verbatimModuleSyntax` | keeps plain imports in the output, so a side-effect import isn't silently dropped | 2026-10-30 |
| typescript-eslint | `no-import-type-side-effects` | `import { type A } from "mod"`, which leaves a stray `import "mod"` after compilation; the fix is `import type { A } from "mod"` | 2026-10-30 |
| eslint | `no-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 import | 2026-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)

## Related

- [AR-05: Why do fallback imports make agents edit the wrong code?](https://beta.llmgraph.in/agent-readable/ar-05.md)
- [AR-02: Do barrel files hide calls from coding agents?](https://beta.llmgraph.in/agent-readable/ar-02.md)
- [CH-03: Why did my plugin registry lose a format after an edit?](https://beta.llmgraph.in/agent-readable/ch-03.md)

## Chimeras that use this rule

- [CH-03: Why did my plugin registry lose a format after an edit?](https://beta.llmgraph.in/agent-readable/ch-03.md)

Last verified 2026-10-30
