Fix: Copier Not Working — Template Updates, Question Conditions, and Migrations
Part of: Python Errors
Quick Answer
How to fix Copier errors — copier.yml not found, conditional questions not appearing, update breaks generated project, migrations between versions, Jinja vs YAML escaping, and answers file conflict.
The Error
You install Copier and try to use a template — fails:
$ copier copy https://github.com/me/my-template ./output
TemplateNotFoundError: copier.yml not found in templateOr a conditional question never appears:
# copier.yml
use_docker:
type: bool
default: no
docker_image:
type: str
when: "{{ use_docker == true }}" # Never askedOr copier update breaks the generated project:
$ copier update
# Merges template changes — but conflicts in your custom code go undetectedOr migrations between template versions don’t run:
_migrations:
- version: 2.0
before:
- rm -rf old_dir$ copier update
# Updates files but `old_dir` is still thereOr YAML/Jinja conflicts in the template:
# copier.yml
project_name:
default: "{{ cookiecutter.project_name }}" # Wrong — cookiecutter syntaxCopier is the modern Python templating tool — built to fix Cookiecutter’s biggest limitation (no template updates after generation). With Copier, generated projects keep a .copier-answers.yml file that tracks the template version and answers; copier update pulls in template changes while preserving local modifications. The tradeoff is more complex configuration (copier.yml is denser than Cookiecutter’s flat JSON) and a steeper learning curve. This guide covers each common issue.
Why This Happens
Copier uses YAML for configuration (instead of Cookiecutter’s JSON), supports conditional questions, has a migrations system for version bumps, and most importantly, can update existing projects when the template changes. The cost of this power: more concepts (questions, conditionals, migrations, tasks, answers files) and more places for things to go wrong.
The update workflow requires a .copier-answers.yml in the generated project — without it, Copier doesn’t know what to update.
Fix 1: Installing and Basic Use
pip install copier
# Or via pipx (recommended for global use)
pipx install copierCopy a template:
# From git
copier copy https://github.com/copier-org/autopretty ./my-project
# From local directory
copier copy ./my-template ./my-project
# Specific version
copier copy --vcs-ref v1.0 https://github.com/me/template ./my-project
# Skip prompts (use defaults)
copier copy --defaults ./my-template ./my-project
# Pass answers as JSON
copier copy --data '{"project_name": "Test"}' ./my-template ./my-projectUpdate a generated project:
cd my-project
copier update
# Pulls latest template version, applies updates, prompts for conflictsCommon Mistake: Using cookiecutter commands with copier (or vice versa). The CLIs are different:
| Cookiecutter | Copier |
|---|---|
cookiecutter ./template | copier copy ./template ./output |
cookiecutter.json | copier.yml |
{{ cookiecutter.X }} | {{ X }} (no namespace) |
| No update support | copier update |
{{ cookiecutter.project_slug }} dir | {{ project_slug }} dir |
If you’re migrating from Cookiecutter, you can’t just rename the config file — the variable references and tool commands all change.
Fix 2: copier.yml Structure
# copier.yml
_min_copier_version: "9.0" # Required Copier version
_subdirectory: "template" # Template files live in subdirectory
_templates_suffix: ".jinja" # Or "" for raw files
# Questions (everything not starting with _ is a question)
project_name:
type: str
help: "Name of the project"
default: "My Project"
project_slug:
type: str
default: "{{ project_name.lower().replace(' ', '_') }}"
description:
type: str
help: "Brief description"
default: ""
multiline: true
python_version:
type: str
default: "3.12"
choices:
- "3.10"
- "3.11"
- "3.12"
use_docker:
type: bool
default: false
framework:
type: str
default: "fastapi"
choices:
fastapi: "FastAPI"
flask: "Flask"
django: "Django"
dependencies:
type: list
default:
- requests
- pydantic
# Conditional question
docker_image_name:
type: str
default: "{{ project_slug }}:latest"
when: "{{ use_docker }}"Recommended layout:
my-template/
├── copier.yml
├── README.md # Documentation for the template itself
└── template/ # Subdirectory listed in _subdirectory
├── README.md # Template README — for generated project
├── pyproject.toml.jinja
└── {{ project_slug }}/
├── __init__.py
└── main.py.jinja_subdirectory: "template" separates template files from template metadata — cleaner than mixing both at the root.
_templates_suffix: ".jinja" means only files ending in .jinja are rendered through Jinja2; other files are copied verbatim. This avoids the “everything is a template, watch out for stray {{ ” problem from Cookiecutter.
Pro Tip: Use the .jinja suffix and _subdirectory pattern from day one. They make templates explicit and prevent the most common Copier issues — files that should be rendered but aren’t (forgot the suffix), and files that shouldn’t be rendered but are (no suffix system at all).
Fix 3: Question Types and Conditional Logic
# String
name:
type: str
default: "Alice"
help: "Your name"
# Integer
age:
type: int
default: 30
validator: "{% if age < 0 %}Age must be positive{% endif %}"
# Boolean
verbose:
type: bool
default: false
# Choice
license:
type: str
default: "MIT"
choices:
- "MIT"
- "Apache-2.0"
- "GPL-3.0"
# Choice with labels and values
framework:
type: str
default: "fastapi"
choices:
"FastAPI (modern, async)": fastapi
"Flask (lightweight)": flask
"Django (batteries included)": django
# Multi-select (list)
features:
type: list
default: []
choices:
- "Authentication"
- "Database"
- "Background jobs"
# Secret (input hidden)
api_key:
type: str
secret: true
help: "Your secret API key"Conditional questions with when:
use_docker:
type: bool
default: false
docker_base_image:
type: str
default: "python:3.12-slim"
when: "{{ use_docker }}" # Only asked if use_docker is true
docker_registry:
type: str
default: "docker.io"
when: "{{ use_docker }}"Validators prevent invalid input:
project_slug:
type: str
default: "{{ project_name.lower().replace(' ', '_') }}"
validator: >-
{% if not project_slug.replace('_', '').isalnum() %}
project_slug must be alphanumeric (underscores allowed)
{% endif %}The validator runs after the user submits — if non-empty string, it’s the error message; if empty, validation passes.
Common Mistake: Forgetting that when expressions are Jinja2, not Python. Use Jinja2’s templating: "{{ use_docker }}" (renders the boolean), not use_docker (raw Python). For more complex conditions: "{{ framework in ['fastapi', 'flask'] }}".
Fix 4: Template Files and Naming
File names support Jinja2 when they end in .jinja or you use a different convention:
template/
├── pyproject.toml.jinja # Rendered, suffix stripped
├── README.md # Copied verbatim (no .jinja suffix)
├── {{ project_slug }}.jinja/ # Directory name templated, contents rendered
│ ├── __init__.py.jinja
│ └── main.py.jinja
└── {% if use_docker %}Dockerfile.jinja{% endif %}Conditional files — wrap the filename in {% if %}:
template/
├── {% if use_docker %}Dockerfile{% endif %}.jinja
├── {% if use_docker %}.dockerignore{% endif %}.jinja
└── {% if framework == 'django' %}manage.py{% endif %}.jinjaIf the condition is false, the filename becomes empty (or evaluates to “.jinja”) — Copier skips creating it.
The _exclude and _skip_if_exists patterns:
# copier.yml
_exclude:
- "*.pyc"
- "__pycache__"
- ".git"
- "{% if not use_docker %}Dockerfile{% endif %}"
- "{% if not use_docker %}.dockerignore{% endif %}"
_skip_if_exists:
- "README.md" # Don't overwrite existing READMEs
- ".env"
- "config/local.py"_exclude removes files from the copy; _skip_if_exists preserves existing files during copy or update.
Pro Tip: Use _skip_if_exists for user-customizable files (config files, env templates, README). Users edit these after generation; without _skip_if_exists, copier update would clobber their customizations.
Fix 5: Update Workflow
After generating a project, Copier writes .copier-answers.yml:
# .copier-answers.yml — auto-generated
_commit: v1.0.0
_src_path: https://github.com/me/template
project_name: My App
project_slug: my_app
framework: fastapi
use_docker: trueThis file is what makes updates possible — Copier knows which template, which version, and what answers were used.
Update workflow:
cd generated-project/
# Pull latest template, re-render with same answers
copier update
# Update to a specific version
copier update --vcs-ref v2.0
# Force update without prompts
copier update --defaults --forceConflict handling — Copier uses three-way merge:
- Read
.copier-answers.ymlto know the old template version - Render template at the old version with the answers → “ancestor”
- Render template at the new version with the answers → “incoming”
- Diff incoming vs ancestor and apply changes to the user’s current files
If a user modified a file that the template also changed, conflicts appear in the file (<<<<<<<, =======, >>>>>>> markers, like git merge conflicts).
Common Mistake: Running copier update on a project without .copier-answers.yml — Copier doesn’t know the source template. The error is clear but newcomers expect copier update to “just figure it out.” Always run copier copy first (which creates the answers file), then copier update thereafter.
Fix 6: Migrations Between Template Versions
When template versions introduce breaking changes (renamed files, removed config), add migration tasks:
# copier.yml
_migrations:
- version: 2.0.0
before:
- "rm -rf old_directory"
- "mv config.yaml config/main.yaml"
after:
- "echo 'Migrated to v2.0'"
- version: 3.0.0
before:
- "python migrate_to_v3.py"Migrations run when the template version crosses the threshold during copier update:
# Project was on v1.5
copier update --vcs-ref v3.0
# Runs v2.0 migration, then v3.0 migration in orderbefore runs before files are updated; after runs after. Useful for cleanup that must happen before the new file layout exists, or initialization that needs the new files in place.
Common Mistake: Migration commands assume Unix shell. On Windows, rm/mv don’t exist. For cross-platform migrations, use Python scripts:
_migrations:
- version: 2.0.0
before:
- python scripts/migrate_v2.py# scripts/migrate_v2.py
import os
import shutil
if os.path.exists("old_directory"):
shutil.rmtree("old_directory")Fix 7: Tasks (Post-Generation Hooks)
Like Cookiecutter’s hooks, Copier supports tasks that run after generation:
_tasks:
- "git init"
- "pre-commit install"
- "python -m pip install -e ."
# Conditional task
- command: "docker build -t {{ project_slug }} ."
when: "{{ use_docker }}"
# Task with description
- command: "{{ _copier_python }} -m pytest"
when: "{{ run_tests_after_gen }}"_copier_python is a special variable pointing to the Python interpreter Copier is running with. Useful for portable Python invocation.
Common Mistake: Tasks fail silently when the command isn’t found. git init requires git to be installed; pre-commit install requires pre-commit to be available. Wrap in shell conditionals or use Python scripts for graceful handling:
_tasks:
- command: "git init && git add . && git commit -m 'Initial' || true"The || true swallows the error if git isn’t installed.
Wrap every task in defensive checks. The task list is code that runs on every contributor’s machine on the day they generate the project — a brittle task is a Slack ticket.
Fix 8: Excluding Files from Updates
Some files should be created once and never touched by updates:
# copier.yml
_skip_if_exists:
- ".env"
- "src/{{ project_slug }}/_user_config.py"
- "README.md"These are skipped when files already exist — copier update won’t overwrite them.
For files Copier should NEVER touch (not even on initial copy):
_exclude:
- "*.pyc"
- ".git/**"
- "docs/generated/**"
- ".venv/**"_exclude patterns are matched against the template files; matching files are skipped during both copy and update.
Production Incident Lens — Template Updates Have Downstream Blast Radius
The reason teams pick Copier over Cookiecutter is copier update. The reason teams later regret picking Copier is also copier update. Every child project pinned to your template inherits whatever you push to the main branch — a careless template change can spread across dozens of services in a single update wave.
Incident pattern — the migration that wasn’t. You ship v2.0 of the template. The change removes an old config file and renames a directory. You add a _migrations block, run copier update in your own test project, see clean diff, merge. Two days later, three downstream teams report broken builds. The cause: their projects were on v1.3 (not v1.9), and the migration only ran the “before” command in v2.0 — they were missing intermediate migrations the template author assumed they had. Their old directory layout never got cleaned up; the new files landed on top of stale state.
Mitigation: chain migrations explicitly.
_migrations:
- version: 1.5.0
before: ["python scripts/migrate_v1_5.py"]
- version: 2.0.0
before: ["python scripts/migrate_v2.py"]
- version: 3.0.0
before: ["python scripts/migrate_v3.py"]Each migration must be idempotent — running it twice on the same project must produce the same result. Otherwise a project that already migrated once (manually) and is now pulling the update will hit duplicate-action failures.
Incident pattern — the answers file that drifted. A contributor on a downstream project edited .copier-answers.yml by hand to change framework: flask → framework: fastapi, then ran copier update. Copier rendered fastapi files on top of an existing flask project; the result was a folder with both app.py (flask) and main.py (fastapi), neither runnable. The team spent half a day untangling it.
Pro Tip: Treat .copier-answers.yml as generated state, not configuration. To change an answer, regenerate the project into a new directory and merge what you actually changed, rather than editing the answers file in place. The exception is purely additive answers (a new question added in a later template version that didn’t exist when you originally baked).
Blast radius checklist before bumping the template major version:
- List every downstream project pinned to the template. If you don’t know, the template needs a registry — a
users.mdlisting every consumer. - For each, confirm which template version they are currently on.
- Write migrations for every intermediate version, not just the latest one.
- Pilot the update on a low-traffic, easily-reverted project before bulk-updating.
- Document the migration in the template release notes — a
v2.0 → v3.0migration guide saves every downstream team from rediscovering the same fix.
Common Mistake: Renaming a question key without a migration. The downstream project’s answers file still has the old key, so on update Copier prompts for the new key (treats it as missing) and the user inadvertently changes the answer. Use migrations to rename keys in .copier-answers.yml before the new template runs.
For long-running shared templates where downstream regenerate is too expensive to coordinate, see Cookiecutter not working — sometimes the simpler “regenerate from scratch” workflow wins because it forces explicit intent at each upgrade.
Still Not Working?
Copier vs Cookiecutter vs Cruft
- Copier — Updates supported, YAML config, conditional questions. Best for templates that evolve.
- Cookiecutter — Simpler, more templates available, no update support.
- Cruft — Cookiecutter + update tracking. Use if you want to stay on Cookiecutter but need updates.
For new templates that you expect to maintain over time, Copier is the better choice. For one-shot generators or migrating an existing Cookiecutter template, sticking with Cookiecutter (or moving to Cruft) is easier.
Testing Copier Templates
# tests/test_template.py
import pytest
from copier import run_copy
def test_default(tmp_path):
result = run_copy(
"./",
str(tmp_path / "generated"),
defaults=True,
unsafe=True, # Allow tasks to run in tests
)
assert (tmp_path / "generated" / "pyproject.toml").exists()
def test_custom_values(tmp_path):
run_copy(
"./",
str(tmp_path / "generated"),
data={"project_name": "Custom"},
defaults=True,
unsafe=True,
)
pyproject = (tmp_path / "generated" / "pyproject.toml").read_text()
assert 'name = "Custom"' in pyprojectunsafe=True is required because tasks (post-gen commands) are gated behind a confirmation in the CLI; tests need to bypass that gate. Without it, every test that exercises a task silently skips it and you ship a broken template.
CI for Template Repos
# .github/workflows/test.yml
name: Test Template
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
- run: pip install copier pytest
- run: pytest tests/
# Test full generation
- run: |
copier copy --defaults . generated/
cd generated/
pip install -e .
pytestCombining with uv / Hatch
Generated projects often use modern Python packaging. For uv-based project setup in templates, see uv not working. For Hatch-based packaging, see Hatch not working.
Versioned Template Releases
Tag your template repo with semver. Users can pin to a specific version:
copier copy --vcs-ref v1.2.0 https://github.com/me/template ./outputIn CI for the template, run integration tests at every tag — guarantees the template generates a working project at each release.
Tag bump = audit obligation. Every minor version of a template that downstream projects use is a change that could ripple. Treat tagging like a library release: bump notes, migration guide, manual smoke test of the bake.
Multi-Template Projects
For projects that combine multiple templates (e.g., backend + frontend + infrastructure), Copier supports separate _subdirectory per template:
copier copy ./backend-template ./my-project --data '{"project_name": "App"}'
copier copy ./frontend-template ./my-project --data '{"project_name": "App"}'
copier copy ./infra-template ./my-project --data '{"project_name": "App"}'Each copier copy writes a separate .copier-answers.yml.<suffix> — copier update --answers-file .copier-answers.yml.backend updates just one template.
Combining with pre-commit
Add .pre-commit-config.yaml.jinja to your template; the post-gen task installs hooks. For pre-commit setup details, see pre-commit not working.
copier update Produces Massive Diffs You Don’t Expect
You ran copier update and the diff touches files you never knew were part of the template — whitespace changes, EOL changes, files that look unchanged from your perspective. Common causes:
- The template repo’s
.gitattributeswas changed and Copier now renders with different line endings. - A previous contributor hand-edited files Copier owns. The update is re-rendering them to the template’s canonical version, surfacing every drift.
_templates_suffixwas changed in the template (e.g.,""→".jinja"). Every file is now classified differently.
Before merging the update, inspect each unexpected change. If a file is full of drift you don’t care about, add it to _skip_if_exists. If the drift represents lost local customizations, decide whether to upstream them to the template or keep them out of Copier’s reach.
Conditional Question Asked Anyway
You set when: "{{ use_docker }}" and Copier still prompts for the docker-specific question. Two common causes:
use_dockerwas answered"no"(a string) instead offalse(a bool). In Jinja,"no"is truthy. Usetype: boolon the question and Copier coerces correctly.- The
whenclause references a question that has not been answered yet because it appears later incopier.yml. Questions are processed top-to-bottom — list the controlling boolean before any conditional questions that depend on it.
_min_copier_version Mismatch Blocks Generation
You bump _min_copier_version: "9.0" in the template; a contributor on Copier 8.x can’t generate it. The error is clear but the friction is real — you’ve just told a chunk of your audience to upgrade before they can use the template. Consider whether the new version actually requires features beyond 8.x, or whether 8.0 would still suffice. Newer is not better if it locks out users.
For containerized template usage where the Copier version is pinned, document the bump in your release notes and pin the same version in any wrapper images.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: Cookiecutter Not Working — Template Errors, Variable Substitution, and Hook Failures
How to fix Cookiecutter errors — cookiecutter.json not found, variable substitution failed Jinja2, pre/post-generation hooks failed, no_input mode missing values, private repo authentication, and copier vs cookiecutter.
Fix: joblib Not Working — Parallel Backends, Memory Cache, and Pickling Errors
How to fix joblib errors — Parallel n_jobs slower than expected, Memory cache miss, backend loky vs threading vs multiprocessing, pickling lambda not supported, dump load file size, and pytest interference.
Fix: Marshmallow Not Working — Schema Errors, Load vs Dump, and Field Validation
How to fix Marshmallow errors — Schema not validated on dump, ValidationError messages format, unknown field handling, missing vs default, post_load object construction, and Marshmallow 3 to 4 migration.
Fix: Pipenv Not Working — Lock File Generation, Shell Activation, and Dependency Resolution
How to fix Pipenv errors — pipenv lock takes forever, Pipfile.lock not generated, shell activation broken, no virtualenv created, dependency conflict, and migration to uv or Poetry.