Python Copier Project Scaffolding — Deep Dive

How the Three-Way Merge Works

Copier’s update mechanism relies on Git’s merge machinery. When you run copier update, the tool:

  1. Checks out the old template version (from .copier-answers.yml) into a temporary branch
  2. Renders it with your saved answers to produce the “old generated” state
  3. Checks out the new template version and renders it with your answers (re-prompting for any new questions)
  4. Computes a diff between old-generated and new-generated
  5. Applies that diff to your working tree using a three-way merge

This means your local changes are preserved as long as they don’t conflict with template changes in the same lines. When they do, you get standard Git conflict markers.

Controlling Merge Behavior

# copier.yml
_conflict: "inline"   # default — Git conflict markers
# _conflict: "rej"    # write .rej files instead

The rej mode is useful when your team prefers to review template updates as separate patch files rather than inline markers.

Template Versioning Strategy

Copier templates should be versioned with Git tags. A disciplined approach:

v1.0.0  — initial template
v1.1.0  — add pre-commit configuration
v2.0.0  — switch from setup.cfg to pyproject.toml (breaking)
v2.1.0  — add GitHub Actions matrix testing

Semver communicates the nature of changes. In copier.yml, constrain which versions users can generate from:

_min_copier_version: "9.0.0"

_vcs_ref: "v2.1.0"  # default version for new projects

Advanced Question Types

Copier’s YAML schema supports sophisticated prompts:

database:
  type: str
  choices:
    PostgreSQL: "postgres"
    SQLite: "sqlite"
    None: ""
  default: "postgres"
  help: "Primary database engine"

port:
  type: int
  default: 8000
  validator: "{% if port < 1024 %}Port must be >= 1024{% endif %}"

extra_dependencies:
  type: yaml
  default: []
  help: "List of additional pip packages"
  multiline: true

The validator field runs Jinja2 — if it renders to a non-empty string, that string becomes the error message. The yaml type lets users input structured data (lists, dicts) directly.

Multi-Template Composition

Large organizations often need layered templates. Copier supports this through sequential application:

# Base template: common files, CI, linting
copier copy gh:org/base-template my-service

# Overlay: FastAPI-specific files
copier copy gh:org/fastapi-overlay my-service

Each copier copy adds its own .copier-answers.yml entry. To avoid collisions, use the _answers_file setting:

# In fastapi-overlay's copier.yml
_answers_file: ".copier-answers-fastapi.yml"

Now copier update can target each layer independently:

copier update --answers-file .copier-answers.yml          # base
copier update --answers-file .copier-answers-fastapi.yml  # overlay

Migration Tasks in Practice

When a template makes breaking changes, migration tasks bridge the gap:

_tasks:
  - command: "python _migrations/v2_rename_src.py"
    when: "{{ _copier_conf.old_version and _copier_conf.old_version < 'v2.0.0' }}"

A real migration script:

# _migrations/v2_rename_src.py
"""Migrate from flat layout to src layout."""
import shutil
from pathlib import Path

old_pkg = Path("mypackage")
new_pkg = Path("src/mypackage")

if old_pkg.exists() and not new_pkg.exists():
    new_pkg.parent.mkdir(parents=True, exist_ok=True)
    shutil.move(str(old_pkg), str(new_pkg))
    print(f"Migrated {old_pkg}{new_pkg}")

Excluding Migration Files from Output

Migration scripts should not end up in generated projects:

_exclude:
  - "_migrations"
  - "copier.yml"
  - "*.pyc"

CI-Enforced Template Compliance

A powerful pattern: run copier update --check in CI to verify that projects stay in sync with their template.

# .github/workflows/template-check.yml
name: Template Compliance
on:
  schedule:
    - cron: "0 9 * * 1"  # Every Monday at 9 AM

jobs:
  check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: pipx install copier
      - run: copier update --skip-answered --defaults --check

If the template has drifted, the check fails, and the team knows it’s time to run copier update and merge the changes.

Copier vs. Cookiecutter vs. Cruft

FeatureCopierCookiecutterCruft
Project generation✅ (wraps Cookiecutter)
Template updates✅ native 3-way merge✅ via Git diff
Typed questions✅ (str, int, bool, yaml)❌ (all strings)❌ (all strings)
Validators✅ Jinja2 expressions❌ (hooks only)
Multi-template✅ separate answer files
Jinja2 file names✅ native exclusion⚠️ via hooks⚠️ via hooks

Cruft occupies a middle ground: it adds update capabilities to existing Cookiecutter templates without rewriting them. But if you’re starting fresh, Copier’s native design is cleaner.

Real-World: Platform Engineering at Scale

Spotify’s Backstage popularized the idea of “software templates” for internal developer portals. Copier fits this pattern well: the platform team maintains versioned templates, developers scaffold from them, and template updates propagate through copier update.

A typical enterprise setup:

  1. Template monorepo with base + overlays (API, worker, frontend)
  2. Template CI that tests generation and validates each overlay against the base
  3. Project CI that runs copier update --check weekly
  4. Migration scripts for every major version bump
  5. Dashboard tracking which projects are on which template version

This turns scaffolding from a one-time convenience into a continuous compliance and standardization tool.

One thing to remember: Copier transforms project templates from disposable starter kits into living infrastructure that evolves with your organization — every project stays connected to its source of truth.

pythonproject-scaffoldingdeveloper-tools

See Also

  • Python Black Formatter Understand Black Formatter through a practical analogy so your Python decisions become faster and clearer.
  • Python Bumpversion Release Change your software's version number in every file at once with a single command — no more find-and-replace mistakes.
  • Python Changelog Automation Let your git commits write the changelog so you never forget what changed in a release.
  • Python Ci Cd Python Understand CI CD Python through a practical analogy so your Python decisions become faster and clearer.
  • Python Cicd Pipelines Use Python CI/CD pipelines to remove setup chaos so Python projects stay predictable for every teammate.