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:
- Checks out the old template version (from
.copier-answers.yml) into a temporary branch - Renders it with your saved answers to produce the “old generated” state
- Checks out the new template version and renders it with your answers (re-prompting for any new questions)
- Computes a diff between old-generated and new-generated
- 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
| Feature | Copier | Cookiecutter | Cruft |
|---|---|---|---|
| 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:
- Template monorepo with base + overlays (API, worker, frontend)
- Template CI that tests generation and validates each overlay against the base
- Project CI that runs
copier update --checkweekly - Migration scripts for every major version bump
- 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.
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.