# 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](https://pydevtools.com/handbook/explanation/modern-python-project-setup-guide-for-ai-assistants.md) is available for direct ingestion into agent contexts.

The conventions below describe how a modern Python project is scaffolded with [uv](https://pydevtools.com/handbook/reference/uv.md) and [pyproject.toml](https://pydevtools.com/handbook/reference/pyproject.toml.md). 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](https://pydevtools.com/handbook/reference/pytest.md) for testing
1. Set up [Ruff](https://pydevtools.com/handbook/reference/ruff.md) for linting and formatting
1. Set up pre-commit (or [prek](https://pydevtools.com/handbook/reference/prek.md)) 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](https://pydevtools.com/handbook/explanation/what-is-pep-621-compatibility.md)-compliant [pyproject.toml](https://pydevtools.com/handbook/reference/pyproject.toml.md) and group dev tools under [PEP 735](https://pydevtools.com/handbook/explanation/what-is-pep-735.md) `[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](https://pydevtools.com/handbook/reference/uvx.md) over global installs. Use `uv run <command>` for tools tracked in `[dependency-groups]` and [uvx](https://pydevtools.com/handbook/reference/uvx.md) `<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](https://pydevtools.com/handbook/explanation/what-is-pep-621-compatibility.md)-compliant [pyproject.toml](https://pydevtools.com/handbook/reference/pyproject.toml.md), pins `requires-python`, and drops in a sensible `.gitignore`.

For applications (scripts, services, tools):

```bash
uv init project-name
cd project-name
```

For packages (libraries, distributable code):

```bash
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](https://pydevtools.com/handbook/explanation/what-is-pypi.md).

### 2. Add runtime dependencies

```bash
# 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](https://pydevtools.com/handbook/reference/pytest.md) as a dev dependency:

```bash
uv add --dev pytest
```

Create the test directory:

```bash
mkdir tests
```

> [!NOTE]
> Modern [pytest](https://pydevtools.com/handbook/reference/pytest.md) doesn't require `tests/__init__.py`; the directory alone is enough for discovery.

Add pytest configuration to `pyproject.toml`:

```toml
[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = ["test_*.py"]
python_functions = ["test_*"]
addopts = ["--strict-markers", "--strict-config"]
```

For coverage, add [coverage.py](https://coverage.readthedocs.io/) and run pytest under it (no plugin required):

```bash
uv add --dev coverage
uv run coverage run -m pytest
uv run coverage report
```

### 4. Configure Ruff for linting and formatting

```bash
uv add --dev ruff
```

Add [Ruff](https://pydevtools.com/handbook/reference/ruff.md) configuration to `pyproject.toml`:

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

```yaml
repos:
  - repo: https://github.com/astral-sh/ruff-pre-commit
    rev: v0.15.21
    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:

```bash
# Classic pre-commit (Python-based)
uvx pre-commit install

# Or prek, the Rust rewrite that runs the same config faster
uvx prek install
```

[prek](https://pydevtools.com/handbook/reference/prek.md) 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:

`````markdown
# 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

```bash
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`:

```toml
[project.scripts]
cli-tool = "cli_tool.main:cli"
```

### Start a web application

```bash
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

```bash
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 Pyrefly

```bash
uv add --dev pyrefly
```

Run type checking:

```bash
uv run pyrefly check
```

Configure Pyrefly in `pyproject.toml`:

```toml
[tool.pyrefly]
python-version = "3.12"
```

If a project already runs [ty](https://pydevtools.com/handbook/reference/ty.md) (OpenAI, beta) or [mypy](https://pydevtools.com/handbook/reference/mypy.md) (established), both remain well-supported alternatives. See [How do mypy, pyright, and ty compare?](https://pydevtools.com/handbook/explanation/how-do-mypy-pyright-and-ty-compare.md) for the trade-offs.

### Add documentation with MkDocs

```bash
uv add --dev mkdocs mkdocs-material
```

### Wire up CI with GitHub Actions

Example workflow (`.github/workflows/test.yml`). The official [`astral-sh/setup-uv`](https://github.com/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.

```yaml
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](https://pydevtools.com/handbook/tutorial/setting-up-github-actions-with-uv.md) tutorial.

## Avoid these anti-patterns

Don't:

- Mix [pip](https://pydevtools.com/handbook/reference/pip.md) and [uv](https://pydevtools.com/handbook/reference/uv.md) in the same project workflow
- Run tools globally instead of with `uv run` or `uvx`
- Use `requirements.txt` with [uv](https://pydevtools.com/handbook/reference/uv.md) projects (use `uv.lock` and, if you need to share a frozen file, `uv export`)
- Manually create [virtual environments](https://pydevtools.com/handbook/explanation/what-is-a-virtual-environment.md); uv creates `.venv` automatically on `uv sync` or `uv run`
- Skip [lockfiles](https://pydevtools.com/handbook/explanation/what-is-a-lock-file.md) when reproducibility matters
- Write dev dependencies into the legacy `[tool.uv.dev-dependencies]` table (use [PEP 735](https://pydevtools.com/handbook/explanation/what-is-pep-735.md) `[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](https://pydevtools.com/handbook/reference/uv.md) manage [virtual environments](https://pydevtools.com/handbook/explanation/what-is-a-virtual-environment.md) automatically
- Pin a `requires-python` floor in `pyproject.toml` (e.g. `requires-python = ">=3.12"`)

## Reference the daily commands

```bash
# 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](https://pydevtools.com/handbook/reference/black.md) .                     # 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
1. Team size: larger teams benefit more from strict linting and enforced hooks
1. Public vs. private: public packages need comprehensive docs, CI, and trusted publishing
1. Performance needs: data and ML projects may need GPU-aware install steps and pinned wheels
1. Deployment target: web apps, CLIs, and notebooks each have different scaffolding needs

## Wire up your editor

### Set up VS Code

Create `.vscode/settings.json`:

```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`
1. Enable [pytest](https://pydevtools.com/handbook/reference/pytest.md) as the default test runner
1. Install the official JetBrains [Ruff](https://pydevtools.com/handbook/reference/ruff.md) plugin (it integrates linting and formatting); see the [plugin docs](https://plugins.jetbrains.com/plugin/20574-ruff) for current configuration steps

## Learn more

- [Claude Code complete guide](https://pydevtools.com/handbook/explanation/claude-code-complete-guide.md)
- [Create your first Python project](https://pydevtools.com/handbook/tutorial/create-your-first-python-project.md)
- [Setting up testing with pytest and uv](https://pydevtools.com/handbook/tutorial/setting-up-testing-with-pytest-and-uv.md)
- [Set up Ruff for formatting and checking your code](https://pydevtools.com/handbook/tutorial/set-up-ruff-for-formatting-and-checking-your-code.md)
- [Setting up GitHub Actions with uv](https://pydevtools.com/handbook/tutorial/setting-up-github-actions-with-uv.md)
- [What is PEP 735 (dependency groups)?](https://pydevtools.com/handbook/explanation/what-is-pep-735.md)
- [What is a virtual environment?](https://pydevtools.com/handbook/explanation/what-is-a-virtual-environment.md)
- [Why should I use a virtual environment?](https://pydevtools.com/handbook/explanation/why-should-i-use-a-virtual-environment.md)
- [Modern Python Tooling Checklist](https://pydevtools.com/tooling-checklist.md), a one-page summary of these defaults
