# Do import aliases confuse coding agents?

> Rule AR-04: Alias imports only to resolve a real clash, and never alias a module to a one-letter name in shared code.

An alias gives one thing two names. A search for `format_amount` misses the file that calls `fa(total)`, and a search for `fa(` matches half the repo. Aliases that the whole ecosystem shares (`np`, `pd`) cost nothing because everyone, agents included, already knows them. Aliases you invent cost a lookup in every file that uses them.

## Why it happens
**Python.** The import statement binds the name after `as` and only that name. For `import foo.bar.baz as fbb` the language reference says the name following `as` is bound directly to the imported module, and `foo` isn't bound at all. For the `from` form, "a reference to that value is stored in the current namespace, using the name in the `as` clause if it is present, otherwise using the attribute name". After the import line, the original name does not exist in that file. Any text search for it fails there, and any tool tracing a call must read the import line first and map `fa` back to `format_amount`.

**TypeScript and JavaScript.** `import { formatAmount as fa } from "./money"` binds only `fa`, and `export { formatAmount as fmt }` publishes the function under a different name than the one it was defined with. The second form is the worse one, because a single rename at the export changes the name every importer must use and nothing in the importers' text matches the definition.

An alias is also a signal. When a file aliases `from a import format_date as format_a`, there is usually a clash upstream (see AR-03), and fixing the name at the source removes the need for the alias.

## Bad and good

```python
# bad: invented aliases, one-letter module names
import billing.money as m
from billing.dates import month_start as ms
from billing.tax import apply_vat as av

def monthly_total(rows):
    return m.format_amount(av(m.sum_rows(rows, ms(rows[0].when))))
```

```python
# good: names stay the same as their definitions
from billing import money
from billing.dates import month_start
from billing.tax import apply_vat

def monthly_total(rows):
    return money.format_amount(apply_vat(money.sum_rows(rows, month_start(rows[0].when))))
```

```ts
// bad
import * as m from "./money";
import { applyVat as av } from "./tax";
export { formatAmount as fmt } from "./money";
```

```ts
// good
import * as money from "./money";
import { applyVat } from "./tax";
export { formatAmount } from "./money";
```

When a clash is real, alias with a name that says where the thing came from:

```python
from billing.utils import format_date as format_invoice_date
from reports.utils import format_date as format_report_date
```

## Trade-offs and when an alias is acceptable
- **Community aliases** (`import numpy as np`, `import pandas as pd`) are shared conventions and are fine. Check them against a list rather than relying on taste. Ruff's `ICN001` enforces the conventional alias from a configured list (the docs example is `pandas` as `pd`).
- **A real clash:** two imports with the same name in one file. The alias should name the origin, as above. The better fix is a rename at the source (AR-03), when you own both.
- **Long names:** a module name that makes every line wrap is a case for importing the submodule (`from concurrent import futures`), not for a one-letter alias.
- **Compatibility shims:** an alias that keeps an old public name alive after a rename is legitimate. Put it in one place, mark it deprecated, and delete it on a schedule.
- **Redundant aliases are different.** `from .x import a as a` is a re-export marker that type checkers read (see AR-02). Don't remove it mechanically.

## Enforce it
| Tool | Setting | What it catches | Checked |
|---|---|---|---|
| ruff | `ICN001` unconventional-import-alias; settings `lint.flake8-import-conventions.aliases` and `extend-aliases` | a module imported under an alias that isn't the configured conventional one | 2026-10-30 |
| ruff | `ICN002` banned-import-alias; setting `lint.flake8-import-conventions.banned-aliases` | aliases you list as banned for a module (the docs example: `tensorflow.keras.backend as K`) | 2026-10-30 |
| ruff | `PLC0414` useless-import-alias | `import numpy as numpy`. The rule doesn't apply in `__init__.py` files, where such aliases are re-exports | 2026-10-30 |
| ruff | `PLR0402` manual-from-import | `import concurrent.futures as futures`, which reads better as `from concurrent import futures` | 2026-10-30 |
| eslint | `no-restricted-syntax` with a selector on `ImportNamespaceSpecifier` and a regular expression on `local.name` | namespace imports bound to a one-letter name. The selector syntax supports `[attr=/regex/]` comparisons, ESLint 9.39.5 accepted this exact selector and flagged only the one-letter namespace name | 2026-10-30 |

No tool checked here bans every alias. The rule is a review rule with ICN001 and ICN002 as the automatic part.

In `pyproject.toml`:
```toml
[tool.ruff.lint]
extend-select = ["ICN001", "ICN002", "PLC0414", "PLR0402"]

[tool.ruff.lint.flake8-import-conventions]
extend-aliases = { "billing.money" = "money" }
banned-aliases = { "billing.money" = ["m", "bm"] }
```
(The mapping shape for `extend-aliases` and `banned-aliases` is `{module = alias}` and `{module = [aliases]}`; ruff 0.17.3 accepted this shape with no config error, and reported ICN001 and ICN002 as expected.)

## Rule block for AGENTS.md / CLAUDE.md
```markdown
- Do not alias imports (`as x`) unless two imported names clash in one file. Use a descriptive
  alias that names the origin (`format_invoice_date`), never a one- or two-letter name.
- Allowed conventional aliases: `np`, `pd`, and the ones listed in pyproject.toml.
- When you meet an alias you didn't write, search for both names before you edit either.
```

## Edge cases
- **Search both names:** when you rename a function that is imported with `as`, the alias line is a caller. Search for the original name in import lines.
- **Aliases in `__init__.py`:** `from .x import a as b` publishes the thing under a different name than its definition. Treat it as a rename and list it where the API is documented.
- **`import x.y as z` versus `from x import y`:** the second leaves the submodule under its own name, which is what PLR0402 asks for.
- **Type-only aliases:** `import type { Invoice as InvoiceRow }` has the same cost, and the same rule applies.
- **Dynamic access through the alias** (`getattr(m, name)`) hides names from every tool at once. See AR-05 and AR-06 for related patterns.

## What LLMGRAPH shows
Calls through `import ... as` aliases print certain against the defining function, so the alias cost falls on grep and human readers. A one-line `getattr(...)(...)` prints `plus 1 dynamic call nearby that could reach anything`.

## References
- Python Language Reference, "The import statement" (`as` binding for both forms): https://docs.python.org/3/reference/simple_stmts.html#the-import-statement
- Ruff ICN001, ICN002: https://docs.astral.sh/ruff/rules/unconventional-import-alias/ · https://docs.astral.sh/ruff/rules/banned-import-alias/
- Ruff PLC0414, PLR0402: https://docs.astral.sh/ruff/rules/useless-import-alias/ · https://docs.astral.sh/ruff/rules/manual-from-import/
- Pyright, "Typed libraries" (redundant alias as a re-export marker): https://github.com/microsoft/pyright/blob/main/docs/typed-libraries.md
- ESLint selectors: https://eslint.org/docs/latest/extend/selectors
- ESTree node definitions: https://github.com/estree/estree/blob/master/es2015.md

## 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-01: Why do star imports make my coding agent miss callers?](https://beta.llmgraph.in/agent-readable/ar-01.md)
- [AR-03: What happens when two functions share a name?](https://beta.llmgraph.in/agent-readable/ar-03.md)
- [AR-02: Do barrel files hide calls from coding agents?](https://beta.llmgraph.in/agent-readable/ar-02.md)

Last verified 2026-10-30
