Skip to content

Instantly share code, notes, and snippets.

@donbr
Created September 28, 2025 18:22
Show Gist options
  • Save donbr/686840ee55082f0bce3307555e5c03b6 to your computer and use it in GitHub Desktop.
Save donbr/686840ee55082f0bce3307555e5c03b6 to your computer and use it in GitHub Desktop.
The Authoritative uv Project Scaffold & Playbook

The Authoritative uv Project Scaffold & Playbook

This repository contains a production-ready Python project starter with uv dependency management, linting, formatting, testing, CI, and contribution guidelines all baked in. It represents a gold-standard foundation for building robust Python applications, designed to eliminate setup friction and enforce quality from the very first commit.

πŸš€ Rollout Strategy: How to Use This Template

There are three recommended ways to use this scaffold, depending on your team's needs.

  1. GitHub Template Repository (Recommended) – For org-wide standards. Create a repository from this scaffold and enable the β€œTemplate repository” setting. Team members can then click Use this template to generate new, fully compliant projects instantly.
  2. Cookiecutter (CLI-driven Templating) – For parameterized, automated scaffolding. Users can generate a new project by running
    pipx run cookiecutter gh:your-org/your-template-repo.
    This automatically renames files and fills in boilerplate details like author name and license.
  3. Bootstrap Script (Onboarding Convenience) – For frictionless local setup. After cloning, a contributor can run a single script (./scripts/bootstrap.sh or .\scripts\bootstrap.ps1) to create the virtual environment, install all dependencies, and set up pre-commit hooks.

πŸ“‚ Project Scaffold Structure

The template is structured as a Cookiecutter project for maximum reusability.


{{ cookiecutter.project_slug }}/
β”œβ”€β”€ .github/
β”‚   └── workflows/
β”‚       └── ci.yml
β”œβ”€β”€ hooks/
β”‚   └── post_gen_project.py
β”œβ”€β”€ scripts/
β”‚   β”œβ”€β”€ bootstrap.ps1
β”‚   └── bootstrap.sh
β”œβ”€β”€ src/
β”‚   └── {{ cookiecutter.project_slug }}/
β”‚       └── **init**.py
β”œβ”€β”€ tests/
β”‚   └── test_example.py
β”œβ”€β”€ .gitignore
β”œβ”€β”€ .pre-commit-config.yaml
β”œβ”€β”€ CONTRIBUTING.md
β”œβ”€β”€ cookiecutter.json
β”œβ”€β”€ pyproject.toml
└── README.md

πŸ“„ File Contents

cookiecutter.json

{
  "project_name": "My Python Project",
  "project_slug": "{{ cookiecutter.project_name | lower | replace(' ', '_') | replace('-', '_') }}",
  "project_description": "A short description of the project.",
  "author_name": "Your Name",
  "author_email": "[email protected]",
  "org_name": "your-org",
  "python_version": ["3.11", "3.12"],
  "license": ["MIT", "Apache-2.0", "GPL-3.0"]
}

hooks/post_gen_project.py

#!/usr/bin/env python3
import os
import subprocess
from pathlib import Path

ROOT = Path.cwd()

def run(cmd, check=True):
    print(f"β†’ {cmd}")
    subprocess.run(cmd, shell=True, check=check)

def maybe_run(cmd):
    try:
        run(cmd, check=True)
    except Exception:
        print(f"(skipped) {cmd}")

def main():
    # Ensure scripts are executable on Unix
    scripts = ["scripts/bootstrap.sh"]
    for s in scripts:
        p = ROOT / s
        if p.exists():
            try:
                os.chmod(p, 0o755)
            except Exception:
                pass

    # Initialize git if not already inside a repo
    if not (ROOT / ".git").exists():
        maybe_run("git init -b main")
        maybe_run("git add .")
        maybe_run('git commit -m "Initialize project from template"')

    print("\nβœ… Project generated.")
    print("Next steps:")
    print("  1) ./scripts/bootstrap.sh   # or scripts\\bootstrap.ps1 on Windows")
    print("  2) source .venv/bin/activate # or .venv\\Scripts\\activate on Windows")
    print("  3) uv run pytest")

if __name__ == "__main__":
    main()

scripts/bootstrap.sh

#!/usr/bin/env bash
set -euo pipefail

usage() {
  cat <<'USAGE'
bootstrap.sh [-h] [--prod] [--no-hooks]

Options:
  --prod       Sync only production deps (no dev/test/lint groups)
  --no-hooks   Skip installing pre-commit hooks
  -h, --help   Show this help
USAGE
}

PROD=false
HOOKS=true
for arg in "$@"; do
  case "$arg" in
    --prod) PROD=true ;;
    --no-hooks) HOOKS=false ;;
    -h|--help) usage; exit 0 ;;
    *) echo "Unknown option: $arg"; usage; exit 2 ;;
  esac
done

echo "πŸ”Ž Checking for uv..."
if ! command -v uv >/dev/null 2>&1; then
  echo "❗ 'uv' not found on PATH. Please install uv: https://github.com/astral-sh/uv"
  exit 1
fi

echo "πŸ”΅ Creating virtual environment (.venv)..."
uv venv

if [ "$PROD" = true ]; then
  echo "πŸ”΅ Syncing production dependencies (no dev/test/lint)..."
  uv sync --no-dev
else
  echo "πŸ”΅ Syncing dev/test/lint dependency groups..."
  # Adjust groups to match your pyproject's [dependency-groups]
  uv sync --group dev --group test --group lint
fi

if [ "$HOOKS" = true ]; then
  echo "πŸ”΅ Ensuring pre-commit is available..."
  if ! command -v pre-commit >/dev/null 2>&1; then
    # Prefer using uv to run it from the venv if not globally installed
    echo "   Installing 'pre-commit' into the environment..."
    uv add --group dev pre-commit >/dev/null 2>&1 || true
  fi

  echo "πŸ”΅ Installing pre-commit hooks..."
  # Use uv run to ensure we use the env-local pre-commit
  uv run pre-commit install
fi

ACTIVATE_HINT="source .venv/bin/activate"
if [[ "${OS:-}" = "Windows_NT" ]] || [[ "$(uname -s || true)" = *"MINGW"* ]]; then
  ACTIVATE_HINT=".venv\\Scripts\\activate"
fi

echo ""
echo "βœ… Environment ready!"
echo "   Activate: $ACTIVATE_HINT"
echo "   Test:     uv run pytest"
echo "   Lint:     uv run ruff check ."
echo "   Format:   uv run black ."

scripts/bootstrap.ps1

#!/usr/bin/env pwsh
$ErrorActionPreference = "Stop"

param(
  [switch]$Prod,
  [switch]$NoHooks
)

function Check-Cmd($cmd) {
  $null -ne (Get-Command $cmd -ErrorAction SilentlyContinue)
}

if (-not (Check-Cmd "uv")) {
  Write-Error "'uv' not found. Install: https://github.com/astral-sh/uv"
}

Write-Host "πŸ”΅ Creating virtual environment (.venv)..."
uv venv

if ($Prod) {
  Write-Host "πŸ”΅ Syncing production dependencies..."
  uv sync --no-dev
} else {
  Write-Host "πŸ”΅ Syncing dev/test/lint dependencies..."
  uv sync --group dev --group test --group lint
}

if (-not $NoHooks) {
  if (-not (Check-Cmd "pre-commit")) {
    Write-Host "   Installing pre-commit into the environment..."
    uv add --group dev pre-commit | Out-Null
  }
  Write-Host "πŸ”΅ Installing pre-commit hooks..."
  uv run pre-commit install
}

Write-Host ""
Write-Host "βœ… Environment ready!"
Write-Host "   Activate: .venv\\Scripts\\Activate.ps1"
Write-Host "   Test:     uv run pytest"
Write-Host "   Lint:     uv run ruff check ."
Write-Host "   Format:   uv run black ."

README.md

# {{ cookiecutter.project_name }}

[![CI Status](https://github.com/{{ cookiecutter.org_name }}/{{ cookiecutter.project_slug }}/actions/workflows/ci.yml/badge.svg)](https://github.com/{{ cookiecutter.org_name }}/{{ cookiecutter.project_slug }}/actions/workflows/ci.yml)
[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
[![Black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
[![Python Version](https://img.shields.io/badge/python-3.11%20%7C%203.12-blue.svg)](pyproject.toml)
[![License: {{ cookiecutter.license }}](https://img.shields.io/badge/License-{{ cookiecutter.license }}-green.svg)](LICENSE)

{{ cookiecutter.project_description }}

## πŸ› οΈ Environment and Dependency Management

This project uses **[uv](https://github.com/astral-sh/uv)** for high-performance Python packaging and dependency management. The `uv.lock` file is the single source of truth for reproducible environments.

[![uv.lock authoritative](https://img.shields.io/badge/dependencies-uv.lock%20authoritative-000?style=for-the-badge&logo=python&logoColor=3776AB)](CONTRIBUTING.md#dependency-management)

### Quick Start

1. **Install `uv`**  
   Follow the [official installation instructions](https://github.com/astral-sh/uv#installation).

2. **Run the bootstrap script**  
   This will create your environment, install dependencies, and set up pre-commit hooks.
   ```bash
   # On macOS/Linux
   ./scripts/bootstrap.sh

   # On Windows (PowerShell)
   .\scripts\bootstrap.ps1
  1. Activate your environment

    # On macOS/Linux
    source .venv/bin/activate
    # On Windows
    .venv\Scripts\activate

For detailed rules on how to add, remove, or update dependencies, please see our Contributing Guide.


### `CONTRIBUTING.md`

```markdown
# Contributing to {{ cookiecutter.project_name }}

First off, thank you for considering contributing! This document outlines our development process and guidelines to make contributing as easy and transparent as possible.

## Getting Started

1. **Run the bootstrap script**  
   This single command sets up your entire local development environment.
   ```bash
   # On macOS/Linux
   ./scripts/bootstrap.sh

   # On Windows (PowerShell)
   .\scripts\bootstrap.ps1
  1. Activate your environment

    # On macOS/Linux
    source .venv/bin/activate
    # On Windows
    .venv\Scripts\activate

Dependency Management

This project uses uv for fast, reproducible dependency management. To maintain stability, all collaborators must adhere to the following rules.

Key Rule: DO NOT edit the [project.dependencies] section of pyproject.toml or the uv.lock file manually. Use the uv command-line tool for all changes.

  • To add a dependency:

    uv add <package-name>
    uv add --group dev <package-name>
  • To remove a dependency:

    uv remove <package-name>
  • To update a dependency:

    uv lock --upgrade-package <package-name>

Code Style & Quality

  • Linting: We use Ruff to check for code quality.

    uv run ruff check .
  • Formatting: We use Black to format our code.

    uv run black .

Running Tests

  • Run the full test suite:

    uv run pytest

Submitting a Pull Request

  1. Fork the repository and create a new branch from main.
  2. Make your changes and add/update tests as appropriate.
  3. Ensure the test suite passes (uv run pytest).
  4. Ensure your code is formatted and linted (uv run black . and uv run ruff check .). Our pre-commit hooks should handle this automatically.
  5. Push your branch and open a pull request.

### `pyproject.toml`

```toml
[project]
name = "{{ cookiecutter.project_slug }}"
version = "0.1.0"
description = "{{ cookiecutter.project_description }}"
authors = [
  { name = "{{ cookiecutter.author_name }}", email = "{{ cookiecutter.author_email }}" }
]
license = { text = "{{ cookiecutter.license }}" }
requires-python = ">=3.11,<3.13"
dependencies = [
  # Add your main dependencies here using `uv add <package>`
]

[project.urls]
Homepage = "https://github.com/{{ cookiecutter.org_name }}/{{ cookiecutter.project_slug }}"

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

[dependency-groups]
dev = [
  "ruff",
  "black",
  "pre-commit"
]
test = [
  "pytest"
]
lint = [
  "ruff"
]

.pre-commit-config.yaml

repos:
  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v4.6.0
    hooks:
      - id: check-yaml
      - id: end-of-file-fixer
      - id: trailing-whitespace

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

  - repo: local
    hooks:
      - id: uv-lock-check
        name: Check uv.lock freshness
        entry: uv lock --check
        language: python
        types: [toml]
        files: ^pyproject\.toml$

.github/workflows/ci.yml

name: CI

on:
  push:
    branches: ["main"]
  pull_request:
    branches: ["main"]

jobs:
  build-and-test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        python-version: ["3.11", "3.12"]

    steps:
      - uses: actions/checkout@v4

      - name: Set up Python ${{ matrix.python-version }}
        uses: actions/setup-python@v5
        with:
          python-version: ${{ matrix.python-version }}

      - name: Install uv
        run: curl -LsSf https://astral.sh/uv/install.sh | sh

      - name: Check lock file freshness
        run: ~/.local/bin/uv lock --check

      - name: Install dependencies
        run: ~/.local/bin/uv sync --group dev --group test --group lint

      - name: Lint with Ruff
        run: ~/.local/bin/uv run ruff check .

      - name: Run tests
        run: ~/.local/bin/uv run pytest

.gitignore

# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST

# Virtual Environment
.venv/
venv/
ENV/
env/
env.bak/
venv.bak/

# uv
.uv_cache/

# IDEs
.idea/
.vscode/

# OS
.DS_Store
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment