
One Template to Rule Them All: Managing Python Projects with Cookiecutter and Cruft
/ 5 min read
Table of Contents
Create Projects Faster and Keep Them Consistent
Starting a new Python project often means repeating the same setup work: creating a Dockerfile, configuring Poetry, organizing folders, adding linting rules, and wiring up development tools. Some projects need small variations, but most of the structure usually stays the same.
Doing that by hand every time is slow and error-prone. That is where Cookiecutter helps. And once you have many projects generated from the same template, Cruft helps you keep them updated.
What is Cookiecutter?
Cookiecutter is a Python tool for generating projects from templates using Jinja syntax. Even though it is a Python package, you can use it to scaffold projects in many languages.
Creating Projects with Cookiecutter
There are two common ways to use Cookiecutter:
- Install and use an existing template.
- Build your own custom template.
This article focuses on the second approach.
To install Cookiecutter, run one of the following:
poetry add cookiecutterpip install cookiecutterconda install -c conda-forge cookiecutterOnce installed, you can start building your template.
Typical Project Structure
Here is an example of the Python project layout I often reuse:
This scaffold lets me create a new project in seconds. It includes:
- A
packagedirectory withscriptsandutils, where the application logic lives. - A
.devcontainerdirectory for Docker and development container configuration. - A
configdirectory for configuration files. - A
datadirectory for datasets, images, and text files. - Repository-level files such as
pyproject.toml,.pre-commit-config.yaml, and ignore files.
These pieces appear in many of my projects, and most of their setup is shared.
Building a Custom Cookiecutter Template
To create a template, add a cookiecutter.json file to your template repository. This file defines the values Cookiecutter will ask for when generating a new project.
{ "project_name": "Name of the project", "project_slug": "Slug of the project", "package_name": "Name of the package", "repository_url": "URL of the project", "project_description": "Description of the project"}This example is intentionally simple, but it is enough for many projects. Each value acts as the default for the prompt shown during project creation.
Cookiecutter uses Jinja syntax, so you can reference values like this:
{{ cookiecutter.project_slug }}In my case, the template uses directories such as {{ cookiecutter.project_slug }} and {{ cookiecutter.project_name }}. Everything inside those folders becomes part of the generated project.
My custom Cookiecutter template, project-wizard, looks like this:
For example, I generate a README automatically from the values defined in cookiecutter.json:
# {{ cookiecutter.project_name }}
{{ cookiecutter.project_description }}In files such as pyproject.toml, I also define packages and tooling that I want in every new repository. The scripts folder can include utility scripts I reuse often. This is where Cookiecutter starts saving real time.
Creating a New Project
Once the template is ready, generate a project with:
cookiecutter .If the template is hosted on GitHub, you can also run:
cookiecutter https://github.com/simoncraf/project-wizard.gitCookiecutter will then prompt you for the values defined in cookiecutter.json. The default values appear in brackets.
At this point you can create as many projects as you want from the same template. The next problem is maintenance: once the template evolves, keeping all generated projects aligned becomes tedious. That is the problem Cruft solves.
What is Cruft?
Cruft is a Python package that keeps generated projects in sync with the Cookiecutter template they came from.
Linking a Project with Cruft
Start by installing Cruft:
poetry add cruftpip install cruftconda install -c conda-forge cruftThen add a .cruft.json file to your Cookiecutter template:
{ "template": "https://github.com/simoncraf/project-wizard", "commit": "069a78495a5d0cbffc8245f42bd190adaa1df698", "checkout": "null", "context": { "cookiecutter": { "project_name": "{{cookiecutter.project_name }}", "project_slug": "{{cookiecutter.project_slug }}", "package_name": "{{cookiecutter.package_name }}", "project_short_description": "{{cookiecutter.project_description }}", "repository_url": "{{cookiecutter.repository_url }}", "_template": "https://github.com/simoncraf/project-wizard" } }, "directory": null}This file stores the information Cruft needs to track the template. The two most important fields are:
template, which points to the Cookiecutter template repository.commit, which identifies the template version used by the project.
When the template changes, its latest commit changes too. Cruft compares the project’s recorded commit with the template and tells you when an update is available.
In my custom template, I also use a helper script to make sure the commit stored in .cruft.json matches the latest template commit:
def get_last_commit_hash(): """ Update the commit hash in `.cruft.json` to the latest commit. """ result = subprocess.run(["git", "log", "-n", "1", "--format=%H"], stdout=subprocess.PIPE) hash_ = result.stdout.decode().strip() file_path = Path("{{cookiecutter.project_slug}}") / ".cruft.json" with file_path.open("r", encoding="utf-8") as f: data = json.load(f) data["commit"] = hash_ with file_path.open("w", encoding="utf-8") as f: json.dump(data, f, indent=4) return hash_If you already created a project from Cookiecutter but it does not include .cruft.json, you can link it manually:
cruft link https://github.com/simoncraf/project-wizardBy default, this points the project to the latest template commit and creates the .cruft.json file for you.
Updating a Project
Once a project is linked, check for updates with:
cruft checkIf there are no updates, you will see a success message:
If the template has changed, Cruft will show that an update is available:
To apply template updates, run:
cruft updateCruft lets you inspect changes, skip them, or apply them directly. After applying updates, it rewrites the corresponding files automatically.
If there are conflicts, Cruft will create .rej files. These work similarly to patch rejects and help you review what could not be merged automatically. Once you resolve or reject the changes, the commit field in .cruft.json is updated to the latest template version.
Resources
Helpful links:
Ready-to-use Cookiecutter templates worth exploring: