Skip to content

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

  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 and verify usage

  • Write a README.md that covers install and development workflow
  • Record the Python choice in .python-version and the supported range in requires-python; lock dependencies in uv.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.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). 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-name

Tip

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 pytest

The --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 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

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

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

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

uv 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 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

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.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 Pyrefly

Use Pyrefly for a new setup, or keep the project’s existing checker:

uv add --dev pyrefly

Configure 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 check

Expect 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-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@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 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": "${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

  1. Point the project interpreter at .venv/bin/python on macOS/Linux or .venv\Scripts\python.exe on Windows
  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
Last updated on