Modern Python Project Setup Guide for AI Assistants
Important
For AI assistants: this guide is meant to be ingested directly. Paste it (or link it) into your project’s CLAUDE.md, AGENTS.md, .cursorrules, or .github/copilot-instructions.md so coding agents follow the same conventions you would. A plain markdown version is available for direct ingestion into agent contexts.
The conventions below 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 --package(oruv init --lib) - Application, script, or service →
uv init(defaults to--app)
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 usage
- Write a README.md that covers install and development workflow
- Pin Python and dependencies via
uv.lock
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). Both keep work inside the project’s locked environment.
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 applications (scripts, services, tools):
uv init project-name
cd project-nameFor packages (libraries, distributable code):
uv init project-name --package
cd project-nameTip
Use --package when creating code meant to be imported by other projects or published to PyPI.
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 pytestCreate 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 report4. 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.15.12
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
README.md template:
# Project Name
Brief description of what the project does.
## Installation
```bash
uv sync
```
## Usage
```bash
uv run python -m 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 # or typer, argparse
uv add --dev pytestAdd the CLI entry point to pyproject.toml:
[project.scripts]
cli-tool = "cli_tool.main:cli"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
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 ty
uv add --dev tyRun type checking:
uv run ty checkConfigure ty in pyproject.toml:
[tool.ty.environment]
python-version = "3.12"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@v4
- uses: astral-sh/setup-uv@v7
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": ".venv/bin/python",
"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/python - 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