End-to-end Python project setup cheatsheet for 2026.

Layout

myproject/
├── pyproject.toml
├── uv.lock
├── README.md
├── .python-version
├── .pre-commit-config.yaml
├── .github/workflows/ci.yml
├── src/myproject/
│   ├── __init__.py
│   └── ...
└── tests/
    ├── conftest.py
    └── test_*.py

Prefer src/ layout — avoids “import works in dev but breaks installed.”

Bootstrap

uv init myproject
cd myproject
uv add fastapi pydantic
uv add --dev pytest pytest-anyio httpx ruff mypy pre-commit

pyproject.toml (complete)

[project]
name = "myproject"
version = "0.1.0"
description = "A description"
authors = [{name = "Me"}]
readme = "README.md"
requires-python = ">=3.13"
dependencies = [
    "fastapi>=0.115",
    "pydantic>=2.9",
]

[dependency-groups]
dev = [
    "pytest>=8",
    "pytest-anyio>=4",
    "httpx>=0.27",
    "pytest-cov>=5",
    "ruff>=0.7",
    "mypy>=1.13",
    "pre-commit>=4",
]

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[tool.hatch.build.targets.wheel]
packages = ["src/myproject"]

[tool.ruff]
target-version = "py313"
line-length = 100

[tool.ruff.lint]
select = [
    "E", "F", "W",      # pycodestyle, pyflakes
    "I",                # isort
    "N",                # pep8-naming
    "UP",               # pyupgrade
    "B",                # bugbear
    "C4",               # comprehensions
    "SIM",              # simplify
    "ARG",              # unused arguments
    "RUF",              # ruff-specific
    "PTH",              # use pathlib
    "TID",              # tidy imports
]
ignore = ["E501"]       # let formatter handle line length

[tool.ruff.lint.per-file-ignores]
"tests/*" = ["S101"]    # allow asserts in tests

[tool.ruff.format]
quote-style = "double"

[tool.mypy]
python_version = "3.13"
strict = true
plugins = ["pydantic.mypy"]

[[tool.mypy.overrides]]
module = ["tests.*"]
disallow_untyped_defs = false

[tool.pytest.ini_options]
testpaths = ["tests"]
addopts = "-ra --strict-markers"
markers = ["slow: long-running tests"]
filterwarnings = ["error"]

[tool.coverage.run]
source = ["src"]
branch = true

[tool.coverage.report]
fail_under = 80
exclude_lines = [
    "pragma: no cover",
    "raise NotImplementedError",
    "if TYPE_CHECKING:",
]

ruff (format + lint)

uv run ruff format .              # format like black
uv run ruff check .               # lint
uv run ruff check . --fix         # auto-fix
uv run ruff check . --fix --unsafe-fixes

mypy

uv run mypy src
uv run mypy src --strict

pytest

uv run pytest
uv run pytest -v
uv run pytest --cov

pre-commit

.pre-commit-config.yaml:

repos:
  - repo: https://github.com/astral-sh/ruff-pre-commit
    rev: v0.7.0
    hooks:
      - id: ruff
        args: [--fix]
      - id: ruff-format

  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v5.0.0
    hooks:
      - id: trailing-whitespace
      - id: end-of-file-fixer
      - id: check-yaml
      - id: check-toml
      - id: check-added-large-files

  - repo: local
    hooks:
      - id: mypy
        name: mypy
        entry: uv run mypy src
        language: system
        pass_filenames: false
        types: [python]
uv run pre-commit install
uv run pre-commit run --all-files

GitHub Actions

.github/workflows/ci.yml:

name: CI
on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        python-version: ["3.13"]
    steps:
      - uses: actions/checkout@v4
      - uses: astral-sh/setup-uv@v3
        with:
          enable-cache: true
      - uses: actions/setup-python@v5
        with:
          python-version: ${{ matrix.python-version }}
      - run: uv sync --frozen
      - run: uv run ruff check .
      - run: uv run ruff format --check .
      - run: uv run mypy src
      - run: uv run pytest --cov --cov-fail-under=80

Dockerfile (production)

# syntax=docker/dockerfile:1.7
FROM python:3.13-slim AS builder
WORKDIR /app
RUN pip install uv
COPY pyproject.toml uv.lock ./
RUN --mount=type=cache,target=/root/.cache/uv \
    uv sync --frozen --no-dev

FROM python:3.13-slim
WORKDIR /app
COPY --from=builder /app/.venv ./.venv
COPY src ./src
ENV PATH=/app/.venv/bin:$PATH
USER 1000:1000
CMD ["python", "-m", "myproject"]

.gitignore

__pycache__/
*.py[cod]
.venv/
.pytest_cache/
.mypy_cache/
.ruff_cache/
.coverage
htmlcov/
dist/
build/
.env

Editor config

.editorconfig:

root = true

[*]
charset = utf-8
end_of_line = lf
indent_style = space
indent_size = 4
trim_trailing_whitespace = true
insert_final_newline = true

[*.{yaml,yml,toml}]
indent_size = 2

Common conventions

  • src/ layout for libraries.
  • pyproject.toml as single source of truth.
  • uv.lock committed.
  • Pre-commit run locally + in CI.
  • mypy strict = true from start (easier than retrofitting).
  • Coverage fail_under = 80 (or whatever your team commits to).

Read this next

That’s 20 Python cheatsheets. Next category: TypeScript.

If you want my full Python project starter (everything above wired), it’s at rajpoot.dev .


Building something AI-, backend-, or data-heavy and want a second pair of eyes? I do consulting and freelance work — see my projects and ways to reach me at rajpoot.dev .