Fix: Copier Not Working — Template Updates, Question Conditions, and Migrations
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.
For pre-commit hook patterns in generated projects, see pre-commit not working.
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.
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. See Cookiecutter not working.
- 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 pyprojectFor pytest fixture patterns with file system testing, see pytest fixture not found.
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.
For Django-specific templates that often use Copier or Cookiecutter, see Django migration conflict.
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.
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.