Fix: Cookiecutter Not Working — Template Errors, Variable Substitution, and Hook Failures
Quick Answer
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.
The Error
You try to generate a project from a template and it fails:
$ cookiecutter https://github.com/me/my-template
A valid repository for "https://github.com/me/my-template" could not be foundOr template variables don’t substitute:
# {{ cookiecutter.project_name }}/main.py
app_name = "{{ cookiecutter.project_name }}" # Renders literally instead of substitutingOr pre-generation hooks fail with no clear error:
$ cookiecutter my-template
ERROR: Stopping generation because pre_gen_project hook script didn't exit successfully
# No further detailsOr the generated directory has a literal {{ cookiecutter.project_slug }} folder name:
my-template/
└── {{ cookiecutter.project_slug }}/ ← Literal name, not substituted
└── main.pyOr --no-input mode skips required variables and breaks:
$ cookiecutter --no-input my-template
# Uses defaults — but a required variable had no default, breaks silentlyCookiecutter is the standard Python project scaffolder — point it at a template (local dir or git URL), answer prompts, and get a generated project. It’s used by Django, Flask, FastAPI, and most Python framework starter kits. The Jinja2-based templating is powerful but template authors hit specific issues around variable scoping, hook execution, and the difference between development and use. This guide covers each.
Why This Happens
Cookiecutter renders a template directory using Jinja2 syntax. The template root must contain cookiecutter.json (defining variables) and at least one directory named with a Jinja2 expression (typically {{ cookiecutter.project_slug }}). Cookiecutter prompts the user for variable values, then renders every file and directory name through Jinja2.
The dual nature — template directory containing literal Jinja2 syntax that becomes a real project after rendering — confuses both template users and template authors.
Fix 1: Installing and Basic Use
pip install cookiecutter
# Or via pipx (recommended for global use)
pipx install cookiecutterGenerate from a template:
# From a git URL
cookiecutter https://github.com/cookiecutter/cookiecutter-django
# From a local directory
cookiecutter ./my-template/
# From a specific branch or tag
cookiecutter https://github.com/me/my-template --checkout v2.0
# Skip prompts (use defaults from cookiecutter.json)
cookiecutter ./my-template/ --no-input
# Override specific values
cookiecutter ./my-template/ project_name="My Project" author_name="Alice"Common Mistake: Pointing cookiecutter at a non-template repo. The repo must contain cookiecutter.json at the root — without it, cookiecutter fails with “valid repository could not be found.” Always verify the repo has a cookiecutter.json before trying to use it as a template.
Generate without prompts in CI:
cookiecutter my-template/ --no-input \
project_name="My App" \
project_slug=my_app \
author_name="CI Bot"Fix 2: cookiecutter.json — Variables and Defaults
{
"project_name": "My Awesome Project",
"project_slug": "{{ cookiecutter.project_name.lower().replace(' ', '_').replace('-', '_') }}",
"author_name": "Your Name",
"email": "[email protected]",
"version": "0.1.0",
"license": ["MIT", "BSD-3-Clause", "Apache-2.0"],
"use_docker": ["yes", "no"],
"_copy_without_render": [
"*.html",
"frontend/templates/*"
]
}Variable types:
| Type | Example | Meaning |
|---|---|---|
| String | "My Project" | Free-text input, default value |
| Choice list | ["MIT", "BSD"] | First value is default; user picks from list |
| Derived | "{{ cookiecutter.x }}_suffix" | Computed from other vars |
| Boolean (yes/no) | ["yes", "no"] | Choice list with yes/no |
Derived variables use Jinja2 — useful for computed defaults:
{
"project_name": "My Project",
"project_slug": "{{ cookiecutter.project_name.lower().replace(' ', '_') }}",
"package_name": "{{ cookiecutter.project_slug }}",
"year": "2025"
}When the user types “Hello World” for project_name, project_slug auto-computes to hello_world.
Private variables (start with _) don’t prompt the user:
{
"project_name": "...",
"_copy_without_render": ["docs/*.html"],
"_extensions": ["jinja2_time.TimeExtension"]
}_copy_without_render lists patterns NOT rendered through Jinja2 — useful for files that contain {{ syntax for other tools (HTML templates, Jinja files in the generated project).
Common Mistake: Forgetting _copy_without_render for HTML or Jinja templates in the generated project. Cookiecutter tries to render them and fails on syntax conflicts. List them in _copy_without_render to copy verbatim.
Fix 3: Template Directory Structure
my-template/
├── cookiecutter.json
├── hooks/
│ ├── pre_gen_project.py # Optional: runs BEFORE generation
│ └── post_gen_project.py # Optional: runs AFTER generation
└── {{ cookiecutter.project_slug }}/ # The actual template directory
├── README.md
├── pyproject.toml
├── {{ cookiecutter.package_name }}/
│ ├── __init__.py
│ └── main.py
└── tests/
└── test_main.pyThe top-level template directory MUST use Jinja2 syntax for its name:
my-template/
├── cookiecutter.json
└── {{ cookiecutter.project_slug }}/ ← Must be a Jinja2 expression
└── ...Without this, cookiecutter generates a literal {{ cookiecutter.project_slug }} directory.
Pro Tip: Always name your top-level template dir {{ cookiecutter.project_slug }} (not {{ cookiecutter.project_name }}). Names contain spaces and special chars; slugs are filesystem-safe. Using project_name produces directories like My Project/ with a space — works on Linux/macOS but trips up many build tools.
File names also support Jinja2:
{{ cookiecutter.project_slug }}/
├── {{ cookiecutter.package_name }}.py # File name templated
└── {% if cookiecutter.use_docker == 'yes' %}Dockerfile{% endif %}Conditional files (file generated only if condition is true) use {% if %} in the name. If the expression evaluates to empty, no file is created.
Fix 4: Jinja2 Templating in Files
Inside files, use standard Jinja2:
# {{ cookiecutter.project_slug }}/main.py
"""{{ cookiecutter.project_name }}
Authored by {{ cookiecutter.author_name }} <{{ cookiecutter.email }}>
"""
VERSION = "{{ cookiecutter.version }}"
{% if cookiecutter.use_docker == "yes" %}
import os
DOCKER_MODE = True
{% else %}
DOCKER_MODE = False
{% endif %}
def main():
print(f"Hello from {VERSION}")
if __name__ == "__main__":
main()Common Jinja2 patterns:
{# This is a Jinja2 comment, doesn't appear in output #}
{# Conditionals #}
{% if cookiecutter.framework == "fastapi" %}
import fastapi
{% endif %}
{# Loops #}
{% for dep in cookiecutter.dependencies.split(",") %}
{{ dep.strip() }}
{% endfor %}
{# String manipulation #}
{{ cookiecutter.project_name | upper }}
{{ cookiecutter.project_name | replace(" ", "-") }}
{{ cookiecutter.project_name | length }}Common Mistake: Forgetting that Cookiecutter renders every text file as Jinja2. If your template contains a file with literal {{ or {% (e.g., a Vue.js template, Django template, GitHub Actions workflow with ${{ }}), the render fails or produces wrong output. Either:
- Add the path to
_copy_without_renderin cookiecutter.json - Escape:
{{ "{{" }}produces literal{{ - Use
{% raw %}...{% endraw %}blocks for sections that should pass through
{# Escape for GitHub Actions syntax #}
{% raw %}
on:
push:
branches: [main]
env:
VERSION: ${{ github.sha }}
{% endraw %}Fix 5: Pre and Post-Generation Hooks
Pre-generation hook runs before rendering — validate inputs, transform variables:
# hooks/pre_gen_project.py
import re
import sys
PROJECT_SLUG = "{{ cookiecutter.project_slug }}"
if not re.match(r"^[a-zA-Z_][a-zA-Z0-9_]*$", PROJECT_SLUG):
print(f"ERROR: '{PROJECT_SLUG}' is not a valid Python module name")
sys.exit(1)Exit non-zero to abort generation.
Post-generation hook runs after rendering — initialize git, install deps, remove unwanted files:
# hooks/post_gen_project.py
import os
import shutil
import subprocess
USE_DOCKER = "{{ cookiecutter.use_docker }}" == "yes"
if not USE_DOCKER:
if os.path.exists("Dockerfile"):
os.remove("Dockerfile")
if os.path.exists(".dockerignore"):
os.remove(".dockerignore")
# Initialize git
subprocess.run(["git", "init"], check=True)
subprocess.run(["git", "add", "."], check=True)
subprocess.run(["git", "commit", "-m", "Initial commit"], check=True)
print("Project created! Next steps:")
print(" cd {{ cookiecutter.project_slug }}")
print(" pip install -e .")Hooks run in the generated directory for post-gen and the template directory for pre-gen. Their working directory matters.
Common Mistake: Using cookiecutter syntax {{ }} in hook scripts and being confused when it works — yes, hook scripts are also rendered through Jinja2 before execution. You can use {{ cookiecutter.use_docker }} directly in hooks/post_gen_project.py — cookiecutter substitutes the value before running the script. Don’t escape it; it’s intended.
Fix 6: Choice Variables and Conditional File Removal
{
"project_name": "My App",
"framework": ["fastapi", "flask", "django"],
"include_docs": ["yes", "no"]
}Use the choice in templates:
# Inside a file
{% if cookiecutter.framework == "fastapi" %}
import fastapi
{% elif cookiecutter.framework == "flask" %}
import flask
{% endif %}Conditional directories — directory name evaluates to empty string when not needed:
{{ cookiecutter.project_slug }}/
├── {% if cookiecutter.include_docs == 'yes' %}docs{% endif %}/
└── src/When include_docs == "no", the docs/ directory is named "" (empty) — cookiecutter skips creating it.
Cleaner pattern with post-gen hook:
# hooks/post_gen_project.py
import shutil
if "{{ cookiecutter.include_docs }}" == "no":
shutil.rmtree("docs")
if "{{ cookiecutter.framework }}" != "django":
shutil.rmtree("django_settings", ignore_errors=True)Easier to maintain than complex Jinja2 expressions in file names.
Fix 7: Template Inheritance and Extensions
For complex templates, use extensions:
{
"_extensions": [
"jinja2_time.TimeExtension",
"slugify.slugify"
]
}pip install jinja2-timeThen in templates:
{# Current year #}
{% now 'utc', '%Y' %}
{# Slugify a string #}
{{ cookiecutter.project_name | slugify }}Reusable templates — split into a base + variants. Two main approaches:
- Multiple cookiecutter templates sharing common files via symlinks or git submodules
- Copier (alternative tool) — supports template inheritance natively
For more advanced templating with versioning and updates after generation, see Copier — it supports updating already-generated projects when the template changes, which cookiecutter doesn’t.
Fix 8: Private Repos and Authentication
# SSH
cookiecutter [email protected]:me/private-template.git
# HTTPS with token
cookiecutter https://oauth2:[email protected]/me/private-template.git
# Cached templates
cookiecutter cookiecutter-pypackage # Use abbreviation if in config~/.cookiecutterrc for abbreviations and defaults:
default_context:
full_name: "Your Name"
email: "[email protected]"
github_username: "yourname"
cookiecutters_dir: "~/.cookiecutters/"
replay_dir: "~/.cookiecutter_replay/"
abbreviations:
pypackage: https://github.com/audreyfeldroy/cookiecutter-pypackage.git
django: https://github.com/cookiecutter/cookiecutter-django.git
gh: https://github.com/{0}.gitThen:
cookiecutter pypackage # Resolves to the full URL
cookiecutter gh:me/my-template # Custom abbreviationdefault_context values pre-fill prompts — useful for personal info you set once.
Cache management:
# Templates cached at ~/.cookiecutters/
ls ~/.cookiecutters/
# Force re-download (skip cache)
cookiecutter my-template --no-cacheStill Not Working?
Cookiecutter vs Copier vs Yeoman
- Cookiecutter — Python-native, Jinja2 templating, simple, mature. Best for one-shot project generation.
- Copier — Python-native too, supports template updates after generation. Best for templates that evolve.
- Yeoman — JavaScript-based, large ecosystem. Best for JS/frontend projects.
For Python projects, Cookiecutter is the dominant standard. Copier is gaining traction for templates that need to support “update existing project to latest template version.”
Testing Templates
pip install pytest-cookies# tests/test_template.py
def test_default(cookies):
result = cookies.bake(extra_context={"project_name": "Test Project"})
assert result.exit_code == 0
assert result.project_path.is_dir()
assert (result.project_path / "README.md").exists()
def test_docker_option(cookies):
result = cookies.bake(extra_context={"use_docker": "yes"})
assert (result.project_path / "Dockerfile").exists()For pytest fixture patterns with cookiecutter 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 cookiecutter pytest pytest-cookies
- run: pytest tests/
# Then bake the template and run the generated project's tests
- run: |
cookiecutter . --no-input --output-dir generated/
cd generated/*/
pip install -e .
pytestThis catches templates that bake successfully but produce broken projects.
Common Generated Project Setups
Common post-gen patterns include initializing git, creating a virtualenv, installing deps. For uv-based project setup, see uv not working. For Hatch-based packaging in generated projects, see Hatch not working.
Cookiecutter Templates Worth Knowing
- cookiecutter-pypackage — Standard Python package
- cookiecutter-django — Django web app
- cookiecutter-data-science — Data science project structure
- cookiecutter-flask — Flask web app
- cookiecutter-pytorch — PyTorch ML project
For Django-specific setup that often starts from a cookiecutter template, see Django migration conflict.
Combining with pre-commit Hooks
Generated projects often include .pre-commit-config.yaml. The post-gen hook can install hooks automatically:
# hooks/post_gen_project.py
import subprocess
subprocess.run(["git", "init"], check=True)
subprocess.run(["pre-commit", "install"], check=False)
# check=False — don't fail if pre-commit isn't installed globallyFor pre-commit setup patterns that work in generated projects, see pre-commit not working.
Updating Generated Projects
Cookiecutter doesn’t support updates — once generated, the project is independent. For long-lived projects that need to track template updates, use Copier (mentioned above) or maintain a cruft overlay:
pip install cruft
# Generate and track
cruft create https://github.com/me/my-template
# Later, pull template updates
cd generated-project/
cruft updatecruft is built on cookiecutter — same templates work, with the addition of update tracking via a .cruft.json file.
Replay Mode
cookiecutter saves all user inputs to ~/.cookiecutter_replay/ after each generation. Re-run with the same inputs:
cookiecutter my-template --replayUseful for re-generating after fixing the template — same context, no need to re-enter every prompt.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: Copier Not Working — Template Updates, Question Conditions, and Migrations
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.
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.