AI-Native Updated 2026-09 8 min read View as Markdown

Structuring a Python repo an agent can navigate

Codebase layout used to be a question of taste. It is now a performance parameter — for your agent and, it turns out, for the humans too.

An agent working on your codebase has a fixed budget of attention. Every file it has to open to answer "where does this belong?" is budget not spent on the change you asked for. Structure that makes the answer obvious is not aesthetic preference any more — it is throughput.

The useful thing is that the properties that help an agent are almost exactly the properties that help a new hire. Nothing here is a compromise made for machines.

The single highest-leverage rule: boundaries an import can express#

An agent decides where to put code by looking at where similar code already lives. If your layout encodes a rule, it follows the rule. If it does not, the agent invents one, and it will invent a different one next week.

src/yourapp/
  core/          pure logic. no I/O, no framework imports, no network.
  adapters/      everything that touches the world: db, http clients, queues, s3
  api/           the web layer. parse -> call core -> serialise. no logic.
  cli/           same, for the terminal
  config.py      settings, read once, passed down
tests/
  core/          fast, offline, no fixtures beyond data
  adapters/      marked integration

The rule is expressible as an import constraint, which means you can enforce it rather than describe it:

pyproject.toml
[tool.ruff.lint.flake8-tidy-imports.banned-api]
"requests".msg = "Use the http client in adapters/, not requests directly."

[tool.importlinter]
root_package = "yourapp"

[[tool.importlinter.contracts]]
name = "core stays pure"
type = "forbidden"
source_modules = ["yourapp.core"]
forbidden_modules = ["yourapp.adapters", "yourapp.api", "requests", "sqlalchemy"]

Now "don't put database calls in core" is a check that fails, not a sentence in a document the agent may or may not weight highly. This is the general pattern worth internalising: a convention you can enforce is worth ten conventions you can only write down.

Files an agent reads first#

In rough order of how often they get opened:

  1. AGENTS.md / CLAUDE.md — always, every session
  2. README.md — usually
  3. pyproject.toml / Makefile — to work out how to run things
  4. The test file matching whatever it is editing
  5. __init__.py of the package it is editing, for the public surface

Make those five accurate and current and you have done most of the work. A stale README is worse than no README: it is confidently wrong context that the agent has no way to distrust.

Naming that survives a grep#

Agents locate code the same way you do when you are new: search. Names that are searchable are worth real money.

  • Prefer distinctive names over short ones. create_subscription beats create. There is one of the first in your codebase and forty of the second.
  • Keep the domain vocabulary consistent. If the database says account, the API says account, and the code says user, every search returns two thirds of the picture. Pick one and put it in AGENTS.md.
  • Name test files after what they test. tests/core/test_pricing.py for src/yourapp/core/pricing.py. An agent asked to change pricing.py will find the test without being told; if the mapping is arbitrary it will write a new test file next to the old one and you will end up with two.

Docstrings, but only where they are unguessable#

Docstrings on obvious functions are noise. Docstrings that record why are the highest-value text in the repo, because "why" is precisely what cannot be recovered from reading the code — by an agent or by you in eight months.

python
def settle_invoice(invoice: Invoice, at: datetime) -> Settlement:
    """Settle an invoice.

    `at` must be timezone-aware UTC. Settlements are recorded against the
    billing period containing `at`, NOT the invoice date — finance reconciles
    on payment date, and changing this breaks the monthly export in ops-reports.
    """

Everything in that second paragraph is invisible in the code and expensive to rediscover. That is the test for whether a docstring earns its place.

Things that reliably get an agent lost#

Deep inheritance hierarchies. Following five levels of super() to find where a method actually lives consumes an enormous amount of context, and the model frequently gives up and overrides at the wrong level. Composition is easier for everyone.

Dynamic attribute magic. setattr loops, __getattr__ fallbacks, string-keyed dispatch dicts built at import time. If static analysis cannot find it, neither can the agent — and neither can your IDE, which should have been the warning.

Giant modules. A 3,000-line utils.py is read in fragments, so the agent sees a slice and duplicates a function that already exists 900 lines away. Splitting by topic fixes it. If you have a utils.py, the things in it belong somewhere with a name.

Multiple ways to do the same thing. Two HTTP clients, two config systems, two date helpers. The agent will pick whichever it happened to see first, and your codebase drifts further apart with every change. Delete one.

Generated code checked in without a marker. Protobufs, OpenAPI clients, migrations. Put them in an obvious directory and say so in AGENTS.md, or you will get careful hand-edits to a file that is overwritten on the next build.

A quick self-test#

Pick a change you have not made yet — "add a discount code to the checkout flow" — and ask an agent, with no extra instruction, where the code should go and which tests it would need to change. Do not let it write anything.

If the answer is right, your structure is legible. If it is wrong, the answer tells you exactly which boundary is unclear, which is more useful diagnostic information than any amount of reading your own repo.

Common questions#

Should I restructure an existing codebase for this?#

Not as a project. Do it opportunistically: when you touch an area, leave it with a clearer boundary than you found. The exception is worth making for utils.py-style dumping grounds and for anywhere your agent has already got lost twice — those have measurable cost.

Do bigger context windows make this irrelevant?#

No, and this is a common mistake. A larger window means more can be loaded, not that more is attended to well; retrieval quality degrades with irrelevant context in the window. Good structure means less needs to be loaded at all, and that stays valuable regardless of window size.

Does this conflict with "just ship it" for small projects?#

Yes, and for a small project you should just ship it. The rules here start paying at around the point where you can no longer hold the whole repo in your head — which is also, not coincidentally, the point where an agent stops being able to either.

Get the Python agent pack

A battle-tested AGENTS.md, the review checklist, and the failure-mode cheat sheet for Python. One email, then occasional updates when the tooling shifts. No course pitch.

Unsubscribe in one click. We never sell the list. Or just take the AGENTS.md now — no email needed.