Skip to content

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 (or uv 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)

  1. Set up pytest for testing
  2. Set up Ruff for linting and formatting
  3. 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.lock for consistent environments
  • Quality-focused: include linting, formatting, and testing from day one
  • Isolated: run everything through uv run or uvx instead 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-name

For packages (libraries, distributable code):

uv init project-name --package
cd project-name

Tip

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 pytest

Create the test directory:

mkdir tests

Note

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 report

4. Configure Ruff for linting and formatting

uv add --dev ruff

Add 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-files

Tip

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 install

prek 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
*.swo

Match 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 pytest

Add 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 httpx

Start a data science project

uv init data-project
cd data-project
uv add pandas numpy matplotlib jupyter
uv add --dev pytest coverage

Verify the setup before shipping

Before completing project setup, verify:

  • pyproject.toml contains all metadata
  • uv.lock is generated
  • .gitignore excludes virtual environments and cache files
  • README.md documents installation and usage
  • Tests directory exists
  • Pre-commit hooks are installed
  • uv run ruff check . passes without errors
  • uv run pytest discovers and runs tests successfully

Layer on common extras

Add type checking with ty

uv add --dev ty

Run type checking:

uv run ty check

Configure ty in pyproject.toml:

[tool.ty.environment]
python-version = "3.12"

Add documentation with MkDocs

uv add --dev mkdocs mkdocs-material

Wire 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 pytest

For 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 run or uvx
  • Use requirements.txt with uv projects (use uv.lock and, if you need to share a frozen file, uv export)
  • Manually create virtual environments; uv creates .venv automatically on uv sync or uv 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 add for runtime dependencies and uv add --dev for dev tools (writes to [dependency-groups])
  • Use uv run for project-local commands and uvx for one-off tools
  • Commit uv.lock for applications, CLIs, and libraries. Current uv guidance is to commit it everywhere
  • Let uv manage virtual environments automatically
  • Pin a requires-python floor in pyproject.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 project

Tailor 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:

  1. Project complexity: simple scripts don’t need full test infrastructure
  2. Team size: larger teams benefit more from strict linting and enforced hooks
  3. Public vs. private: public packages need comprehensive docs, CI, and trusted publishing
  4. Performance needs: data and ML projects may need GPU-aware install steps and pinned wheels
  5. 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

  1. Point the project interpreter at .venv/bin/python
  2. Enable pytest as the default test runner
  3. Install the official JetBrains Ruff plugin (it integrates linting and formatting); see the plugin docs for current configuration steps

Learn more

Last updated on