Modern Python Project Setup Guide for AI Assistants
Important
For coding agents: read this guide when creating a project. For an existing repository, follow its checked-in tooling and instructions first. Put the commands that actually work in a short project instruction file; a link alone does not guarantee that an agent retrieves or reads a page. A plain markdown version is available for tools that accept external context.
The conventions describe how a modern Python project is scaffolded with uv and pyproject.toml. Each step maps to a detailed section later in the guide.
Walk the decision tree
Follow this flow when setting up a project:
Step 1: Determine project type
- Library or package for distribution →
uv init --lib(no console script) oruv init(adds one) - Script or internal tool you never distribute →
uv init --no-package(flat layout, no build system)
Step 2: Add runtime dependencies
- Project needs external packages →
uv add <package> - No dependencies yet → skip to Step 3
Step 3: Configure development tools (in order)
- Set up pytest for testing
- Set up Ruff for linting and formatting
- Set up pre-commit (or prek) hooks for automation
Step 4: Document and verify usage
- Write a README.md that covers install and development workflow
- Record the Python choice in
.python-versionand the supported range inrequires-python; lock dependencies inuv.lock - Record setup and check commands in the agent’s project instructions
Give each agent the project contract
Use the instruction format supported by the client that runs the task:
| Client | Project instructions |
|---|---|
| Codex | AGENTS.md |
| Claude Code | CLAUDE.md, or AGENTS.md according to its project-instructions mode |
| Cursor | AGENTS.md or project rules in .cursor/rules/*.mdc |
| GitHub Copilot | .github/copilot-instructions.md; support for AGENTS.md depends on the client and feature |
| Gemini CLI | GEMINI.md; other filenames require context.fileName configuration |
Keep the setup command, check commands, and source/test paths in those files. Put optional reusable procedures in skills when the client supports them. Instructions provide context; hooks run automation, permissions control tool access, and CI verifies the committed result. They serve different purposes.
Start with writing AGENTS.md for a Python project, configuring Claude Code for uv, or writing Cursor project rules. Check Copilot’s instruction support for the surface in use.
Prepare the environment where the task runs
A local terminal, an editor agent, a worktree, and a cloud task may each use a different checkout and environment. Installing dependencies on a laptop does not prepare a remote runner.
For an existing uv project with a committed lockfile, run this in the task’s checkout:
uv sync --locked
uv run --locked python -c "import sys; print(sys.executable)"uv run executes the command inside the project’s virtual environment, ensuring all dependencies are installed first. Expect an environment at that checkout’s .venv and an interpreter path inside it, unless the project explicitly overrides the environment location.
--locked rejects dependency metadata that no longer matches the lockfile. Provision uv, the required Python, and any private-index credentials through the runner’s setup and secrets controls. Confirm package downloads work during setup when task-time network access is restricted.
Keep a separate environment for each worktree. Run parallel agents with isolated worktrees when tasks edit independently.
Pin the core principles
A modern Python project setup should be:
- Automated: minimize manual configuration steps
- Standardized: declare metadata in a PEP 621-compliant pyproject.toml and group dev tools under PEP 735
[dependency-groups] - Reproducible: lock dependencies via
uv.lockfor consistent environments - Quality-focused: include linting, formatting, and testing from day one
- Isolated: run everything through
uv runoruvxinstead of relying on global installs
Note
Prefer uv run and uvx over global installs. Use uv run <command> for tools tracked in [dependency-groups] and uvx <command> for one-off tools (like pre-commit install). uv run uses the project environment; uvx runs tools in a separate environment and does not use the project lockfile to select their versions.
Initialize a standard project
1. Create the project structure
Tip
uv init handles the basics. It writes a PEP 621-compliant pyproject.toml, pins requires-python, and drops in a sensible .gitignore.
For a flat script or internal tool:
uv init --no-package project-name
cd project-name--no-package keeps the project layout flat (a main.py at the top level instead of a src/ package).
For packages (libraries, distributable code):
uv init project-name
cd project-nameTip
This is the default: a src/ layout with a build system, ready to be imported by other projects or published to PyPI. On uv releases from before uv init became packaged-by-default, pass --package.
2. Add runtime dependencies
# Add dependencies as needed
uv add requests pandas
# Specify version constraints when needed
uv add "django>=4.2,<5.0"3. Configure testing with pytest
Add pytest as a dev dependency:
uv add --dev pytestThe --dev flag marks pytest as a development dependencyA package needed during development (testing, linting, formatting) that is not shipped to users of your project. Installed with the --dev flag.
, keeping it out of production installs.
Create the test directory:
mkdir testsNote
Modern pytest doesn’t require tests/__init__.py; the directory alone is enough for discovery.
Add pytest configuration to pyproject.toml:
[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = ["test_*.py"]
python_functions = ["test_*"]
addopts = ["--strict-markers", "--strict-config"]For coverage, add coverage.py and run pytest under it (no plugin required):
uv add --dev coverage
uv run coverage run -m pytest
uv run coverage reportAdd runnable tests before running these commands; an empty tests/ directory produces pytest exit code 5, not a passing result.
4. Configure Ruff for linting and formatting
uv add --dev ruffAdd Ruff configuration to pyproject.toml:
[tool.ruff]
line-length = 88
target-version = "py312"
[tool.ruff.lint]
select = [
"E", # pycodestyle errors
"F", # pyflakes
"I", # isort
"B", # flake8-bugbear
"C4", # flake8-comprehensions
"UP", # pyupgrade
]
ignore = [
"E501", # line length (handled by formatter)
]
[tool.ruff.format]
quote-style = "double"
indent-style = "space"5. Set up pre-commit (or prek) hooks
Create .pre-commit-config.yaml:
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.17.2
hooks:
- id: ruff-check
args: [--fix]
- id: ruff-format
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v6.0.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-toml
- id: check-added-large-filesTip
Check the upstream release pages before copying these rev: values into a real project; both repositories cut tags often and a stale rev is the easiest way to make a setup look unmaintained on day one.
Install hooks with either runner:
# Classic pre-commit (Python-based)
uvx pre-commit install
# Or prek, the Rust rewrite that runs the same config faster
uvx prek installprek is a drop-in replacement for the pre-commit CLI and reads the same .pre-commit-config.yaml.
6. Create standard project files
For a packaged project created with uv init project-name, use its generated console command in the README. For the flat --no-package layout, substitute uv run python main.py in the usage section:
# Project Name
Brief description of what the project does.
## Installation
```bash
uv sync
```
## Usage
```bash
uv run project-name
```
## Development
```bash
# Run tests
uv run pytest
# Format code
uv run ruff format .
# Lint code
uv run ruff check .
```Extend .gitignore (if needed beyond what uv init provides):
# uv init already includes Python basics
# Add project-specific items:
# Testing
.pytest_cache/
.coverage
htmlcov/
# IDE (if not already present)
.vscode/
.idea/
*.swp
*.swoMatch the project to a starter pattern
Start a CLI tool
uv init cli-tool --package
cd cli-tool
uv add click # argparse is in the standard library and needs no dependency
uv add --dev pytestuv init creates the cli-tool console entry point in [project.scripts]. Implement the command at that entry point instead of adding a second table or referring to a function that does not exist.
Start a web application
uv init web-app
cd web-app
uv add fastapi uvicorn # or django, flask
uv add --dev pytest pytest-asyncio httpxStart a data science project
uv init data-project
cd data-project
uv add pandas numpy matplotlib jupyter
uv add --dev pytest coverageVerify the setup before shipping
Report the exact commands, exit codes, and checks that could not run. A passing formatter does not prove tests passed; pytest reporting no tests is not a successful test run.
Before completing project setup, verify:
-
pyproject.tomlcontains all metadata -
uv.lockis generated -
.gitignoreexcludes virtual environments and cache files -
README.mddocuments installation and usage - Tests directory exists
- Pre-commit hooks are installed
-
uv run ruff check .passes without errors -
uv run pytestdiscovers and runs tests successfully
Layer on common extras
Add type checking with Pyrefly
Use Pyrefly for a new setup, or keep the project’s existing checker:
uv add --dev pyreflyConfigure Pyrefly before checking the project. Add this table to pyproject.toml:
[tool.pyrefly]
preset = "default"Without native or migratable checker configuration, Pyrefly selects a basic preset that silences many type diagnostics. An explicit preset makes the intended checking behavior clear.
uv run pyrefly checkExpect either a report of type errors or a summary with zero errors for the configured project scope.
If a project already runs ty (Astral, beta) or mypy (established), both remain well-supported alternatives. See How do mypy, pyright, and ty compare? for the trade-offs.
Add documentation with MkDocs
uv add --dev mkdocs mkdocs-materialWire up CI with GitHub Actions
Example workflow (.github/workflows/test.yml). The official astral-sh/setup-uv action installs uv, restores the uv cache, and lets uv manage the Python install, so actions/setup-python is no longer needed.
name: Test
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7.0.1
- uses: astral-sh/setup-uv@v10.3.3
with:
enable-cache: true
- name: Install dependencies
run: uv sync --locked
- name: Lint
run: uv run ruff check .
- name: Test
run: uv run pytestFor a more thorough walkthrough including matrix builds and caching strategy, see the Setting up GitHub Actions with uv tutorial.
Avoid these anti-patterns
Don’t:
- Mix pip and uv in the same project workflow
- Run tools globally instead of with
uv runoruvx - Use
requirements.txtwith uv projects (useuv.lockand, if you need to share a frozen file,uv export) - Manually create virtual environments; uv creates
.venvautomatically onuv syncoruv run - Skip lockfiles when reproducibility matters
- Write dev dependencies into the legacy
[tool.uv.dev-dependencies]table (use PEP 735[dependency-groups]instead)
Do:
- Use
uv addfor runtime dependencies anduv add --devfor dev tools (writes to[dependency-groups]) - Use
uv runfor project-local commands anduvxfor one-off tools - Commit
uv.lockfor applications, CLIs, and libraries. Current uv guidance is to commit it everywhere - Let uv manage virtual environments automatically
- Pin a
requires-pythonfloor inpyproject.toml(e.g.requires-python = ">=3.12")
Reference the daily commands
# Development workflow
uv run pytest # Run tests
uv run coverage run -m pytest # Run tests under coverage
uv run coverage report # Show coverage results
uv run ruff check . # Lint code
uv run ruff check --fix . # Lint and auto-fix
uv run ruff format . # Format code
# Dependency management
uv add package-name # Add runtime dependency
uv add --dev package-name # Add dev dependency
uv remove package-name # Remove dependency
uv sync # Sync environment with lockfile
uv lock # Update lockfile
# Project execution
uv run python script.py # Run script
uv run python -m module # Run module
uv run --with package cmd # Run with temporary dependency
# One-off tool execution
uvx pre-commit install # Install pre-commit hooks
uvx black . # Run black without adding to projectTailor advice to context
Adjust the recommendations to fit the project. A throwaway script doesn’t need pre-commit hooks; a library shipped to PyPI does. Some factors worth weighing:
- Project complexity: simple scripts don’t need full test infrastructure
- Team size: larger teams benefit more from strict linting and enforced hooks
- Public vs. private: public packages need comprehensive docs, CI, and trusted publishing
- Performance needs: data and ML projects may need GPU-aware install steps and pinned wheels
- Deployment target: web apps, CLIs, and notebooks each have different scaffolding needs
Wire up your editor
Set up VS Code
Create .vscode/settings.json:
{
"python.defaultInterpreterPath": "${workspaceFolder}/.venv",
"python.testing.pytestEnabled": true,
"python.testing.pytestArgs": ["tests"],
"[python]": {
"editor.defaultFormatter": "charliermarsh.ruff",
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.organizeImports": "explicit"
}
}
}Set up PyCharm
- Point the project interpreter at
.venv/bin/pythonon macOS/Linux or.venv\Scripts\python.exeon Windows - Enable pytest as the default test runner
- Install the official JetBrains Ruff plugin (it integrates linting and formatting); see the plugin docs for current configuration steps