Template Structure
July 6, 2026 · View on GitHub
A Cookiecutter template is a directory with a specific structure. This guide describes each component in detail.
Template Anatomy
my-template/
├── cookiecutter.json # Variables and defaults
├── hooks/ # Pre/post generation scripts (optional)
│ ├── pre_gen_project.py
│ └── post_gen_project.py
├── {{ cookiecutter.project_name }}/ # Template directory (Jinja2 in the name)
│ ├── {{ cookiecutter.module_name }}.py
│ ├── README.md
│ ├── setup.py
│ └── ...
└── docs/ # Template docs (not generated)
cookiecutter.json
Defines the variables the user fills in during generation. It is a plain JSON where each key is a variable and each value is the default.
Value Types
{
"project_name": "My Project",
"project_slug": "my_project",
"author_name": "Your Name",
"version": "0.1.0",
"use_database": ["no", "sqlite", "postgresql"],
"license": ["MIT", "BSD-3", "GPLv3", "Apache-2.0", "proprietary"],
"python_version": "3.11"
}
- String → free text prompt with that default.
- Array → selection prompt (list of choices).
- Number → numeric prompt.
- Boolean → not natively supported; use
"y"/"n"or arrays["yes", "no"].
Derived Variables (Jinja2 in JSON)
Cookiecutter supports Jinja2 inside cookiecutter.json to compute derived values:
{
"project_name": "My Project",
"project_slug": "{{ cookiecutter.project_name|lower|replace(' ', '_')|replace('-', '_') }}",
"pkg_name": "{{ cookiecutter.project_slug|replace('_', '') }}"
}
When entering project_name = "My Awesome Project":
project_slug→my_awesome_project(auto-computed, not prompted).pkg_name→myawesomeproject.
Note: Derived variables with Jinja2 are not shown in the interactive prompt. They are computed automatically.
The {{ cookiecutter.xxx }}/ Directory
The template root directory uses Jinja2 in its name. When generated, it is replaced by the variable value:
{{ cookiecutter.project_name }}/ → My Project/
Jinja2 in File and Subdirectory Names
{{ cookiecutter.project_name }}/
├── {{ cookiecutter.module_name }}.py
├── {{ cookiecutter.author|lower }}_config.yaml
└── tests/
└── test_{{ cookiecutter.module_name }}.py
Jinja2 in File Content
Any file inside the template is processed with Jinja2:
# setup.py
from setuptools import setup
setup(
name="{{ cookiecutter.project_slug }}",
version="{{ cookiecutter.version }}",
author="{{ cookiecutter.author_name }}",
)
# README.md
# {{ cookiecutter.project_name }}
Author: {{ cookiecutter.author_name }}
License: {{ cookiecutter.license }}
Conditional Files
Cookiecutter does not natively support conditionals on file existence. This is handled with:
post_gen_project.pyhooks — delete files based on conditions.- Jinja2 conditionals inside the file — leave content empty.
Example with post-gen hook
# hooks/post_gen_project.py
import os
import shutil
if not {{ cookiecutter.use_docker|tojson }}:
os.remove("Dockerfile")
shutil.rmtree("docker/")
Example with Jinja2 conditional
{% if cookiecutter.use_docker == "yes" %}
FROM python:{{ cookiecutter.python_version }}-slim
WORKDIR /app
COPY . .
RUN pip install -r requirements.txt
{% endif %}
hooks/ Directory
Contains Python scripts that run before and after generating the project.
hooks/
├── pre_gen_project.py # Before creating the project
└── post_gen_project.py # After creating the project
See Hooks Guide for full details.
Special Files and Directories
_copy_without_render
A file named _copy_without_render at the template root lists paths that are NOT processed with Jinja2:
# _copy_without_render
*.html
docs/_build/
node_modules/
*.min.js
Useful for files that contain {{ }} but are not Jinja2 (e.g., Angular, Vue, Go templates).
_extensions/
Directory for custom Jinja2 filters:
_extensions/
└── my_filters.py
# _extensions/my_filters.py
def slugify(value):
return value.lower().replace(' ', '-').replace('_', '-')
Usage in cookiecutter.json:
{
"_extensions": ["my_filters.slugify"],
"project_slug": "{{ cookiecutter.project_name|slugify }}"
}
Naming Conventions
| Variable | Convention | Example | Typical Use |
|---|---|---|---|
project_name | Human readable | My Awesome Project | README, docs |
project_slug | snake_case | my_awesome_project | directory names, imports |
pkg_name | no separators | myawesomeproject | PyPI, setup.py |
repo_name | kebab-case | my-awesome-project | GitHub repo name |
module_name | snake_case | core | main module name |