AGENTS.md — a project constitution for AI-assisted Python development
AI coding agents start every session with zero knowledge of your project. A project constitution fixes that. Here's a copy-paste template for Python projects using UV.
Contents
Every AI coding agent — Claude Code, Cursor, Copilot — starts each session with zero knowledge of your project. It doesn’t know your package manager, your test command, or your directory layout. So it guesses. And it guesses wrong.
A project constitution fixes this. It’s a markdown file at the root of your repo that loads automatically at session start. Anthropic calls theirs CLAUDE.md. The vendor-neutral version is AGENTS.md. Same idea — a single file that turns a generic assistant into one that knows your project.
What goes in it
Four things. Keep it short — agents deprioritize long context.
- Commands — how to run, test, lint, and format. If the agent can verify its own work, it will.
- Project structure — a quick tree so the agent knows where files go.
- Conventions — naming, typing, error handling. Be specific: “use type hints on all public functions” beats “write good code.”
- Constraints — what not to do. Ban
pip, banrequirements.txt, banprint()debugging. Agents repeat mistakes you don’t explicitly forbid.
Start minimal. Add rules only when you see the agent making the same mistake twice.
The template
A full AGENTS.md for a Python project using UV and Ruff. Adapt the structure and dependencies to your own repo.
# AGENTS.md
> Project constitution for AI-assisted development.
## Commands
```bash
uv run pytest # Run test suite
uv run pytest -x # Stop on first failure
uv run ruff check . # Lint
uv run ruff format . # Format
uv run python src/main.py # Run the application
```
> Do NOT use pip, pip install, or requirements.txt. This project uses UV.
> Do NOT create virtual environments manually. UV manages environments automatically.
## Project structure
```
my-project/
pyproject.toml # Single source of truth for deps and config
uv.lock # Lockfile — do not edit manually
src/
main.py # Entry point
devices/
inventory.py # Device inventory loader
connections.py # Connection factory
templates/
base_config.j2 # Jinja2 base template
utils/
logging.py # Structured logging setup
tests/
conftest.py # Shared fixtures
test_inventory.py
test_connections.py
inventory/
hosts.yaml # Device inventory
```
## Dependencies
- Python 3.12+
- UV for package and environment management
- Ruff for linting and formatting
- pytest for testing
Adding a dependency:
```bash
uv add <package> # Adds to pyproject.toml and updates uv.lock
uv add --dev <package> # Dev dependency
```
## Code style
- **Type hints** on all function signatures. Use `from __future__ import annotations` at the top of every module.
- **Docstrings** on public functions and classes. Google style.
- **Snake_case** for functions, variables, and modules. **PascalCase** for classes.
- **f-strings** for string formatting. No `.format()` or `%` formatting.
- **pathlib.Path** for file paths. No `os.path`.
- **Structured logging** via the `logging` module. No bare `print()` statements.
- Functions should do one thing. If a function exceeds 30 lines, consider splitting it.
## Error handling
- Catch specific exceptions, never bare `except:`.
- Network operations (SSH, API calls) must have explicit timeout parameters.
- Use `contextlib.suppress()` for expected exceptions, not try/except/pass.
- Re-raise unexpected exceptions. Do not silently swallow errors.
## Design patterns
- **Protocols over inheritance.** Use `typing.Protocol` to define interfaces.
- **Context managers** for anything that needs cleanup — files, connections, temporary state.
- **Dataclasses for internal state, Pydantic for boundaries.** Use `dataclasses.dataclass` for value objects. Use Pydantic `BaseModel` when parsing external input.
- **Dependency injection** for testability. Pass collaborators as parameters instead of constructing them internally.
- **Composition over inheritance.** Small, focused functions that compose together.
## Testing
- Every new module gets a corresponding test file in `tests/`.
- Use fixtures for device connections and shared state.
- Mock external network calls in unit tests. Integration tests can hit lab devices.
- Test names describe the scenario: `test_inventory_loads_from_yaml`, not `test_inventory_1`.
## Constraints
- Do NOT use pip, virtualenv, poetry, or pipenv. UV only.
- Do NOT create requirements.txt files.
- Do NOT use `os.system()` or `subprocess.run()` for tasks UV can handle.
- Do NOT commit `.env` files or credentials. Use environment variables.
- Do NOT modify uv.lock manually.
- Ruff configuration lives in pyproject.toml, not in separate config files.
Why UV?
UV is a Python package and project manager written in Rust by the Astral team (the same people behind Ruff). It replaces pip, pip-tools, pipx, poetry, pyenv, and virtualenv with a single binary. It uses pyproject.toml as the single source of truth and generates a uv.lock lockfile for reproducible builds.
Getting started
uv init my-project
cd my-project
uv add netmiko napalm jinja2
uv add --dev pytest ruff
Drop the template into the root of your repo, edit the structure to match your setup, and every AI agent that touches the project starts with the right context.