Sprout
Sprout is a Jinja2-based project generator with a Python manifest.
Instead of configuring prompts in
YAML, you write sprout.py:
from sprout import Question
questions = [
Question(key="project_name", prompt="Project name"),
]
That single manifest drives interactive prompts, CLI flags, validation and conditional questions.
Template files go in template/. .jinja files are rendered, everything else
is copied.
Works with local templates, Git repos, or owner/repo GitHub shorthand, including templates in a
repository subdirectory.
Every question becomes a CLI flag so you can script it too:
sprout new <template-path> <project-path>
sprout new <template-path> <project-path> --project-name demo
Install
Install globally with uv:
uv tool install sprout-template
Or run directly without installing using uvx:
uvx --from sprout-template sprout <command>
Usage
sprout init [directory]
sprout add <template-source> [--name <trusted-name>]
sprout list
sprout new <template> <project-path> [--force] [--<question-flag> <value> ...]
new accepts a local template path, Git URL, owner/repo GitHub shorthand, or a trusted name added
with sprout add. For a template in a repository subdirectory, use owner/repo/path/to/template or
<git-url>#path/to/template; only that subdirectory is checked out. Pass values for question flags
to skip those prompts:
sprout new <template-path> <project-path> --project-name demo
Use sprout new <template> --help to show template-specific flags.
Initialize a template
Create a minimal sprout.py and template/README.md.jinja scaffold in the current directory:
sprout init
Pass a directory to initialize it elsewhere. Existing scaffold files are never overwritten.
Trusted templates
Store a reusable name for any supported template source:
sprout add zigai/python-project-template --name py
sprout new py ./my-project
sprout list
Template structure
The template root (the source, or its repository subdirectory) must contain sprout.py.
The only required name is questions.
from sprout import Question
questions = [
Question(key="project_name", prompt="Project name"),
]
Optional names are template_dir, style, extensions, title, cli_boolean_style,
should_skip_file(...), and apply(context).
Question model
Each Question describes one answer:
from sprout import Question
Question(
key="project_name",
prompt="Project name",
help="Used for package metadata and generated paths",
default="demo",
metavar="name",
)
key is the answer dictionary key. It also becomes the CLI flag name.
project_name -> --project-name
Choices
Use choices when the answer should come from a closed list:
from sprout import Question
questions = [
Question(
key="package_manager",
prompt="Package manager",
choices=[("uv", "uv"), ("pip", "pip")],
default="uv",
),
]
Multiselect
from sprout import Question
questions = [
Question(
key="workflow",
prompt="Workflows",
choices=[("tests", "Tests"), ("lint", "Lint")],
multiselect=True,
),
]
From the CLI:
sprout new <template-path> <project-path> --workflow tests --workflow lint
Booleans
Use the built-in yes/no helper:
from sprout import Question
questions = [
Question.yes_no(
key="git_init",
prompt="Initialize Git?",
default=True,
),
]
By default, yes/no questions are exposed as Boolean CLI flags:
sprout new <template-path> <project-path> --git-init
sprout new <template-path> <project-path> --no-git-init
In --help, paired boolean flags are documented using bracket negation: --[no-]git-init.
If a template should use explicit yes/no values instead, opt into that style in sprout.py:
cli_boolean_style = "yes-no"
Then the CLI accepts values for yes/no questions:
sprout new <template-path> <project-path> --git-init yes
sprout new <template-path> <project-path> --git-init no
Conditional flow
when can be a boolean or a callable that receives the answers collected so far:
from sprout import Question
questions = [
Question.yes_no(
key="create_github_repo",
prompt="Create GitHub repository?",
default=False,
),
Question(
key="github_repo_visibility",
prompt="GitHub repository visibility",
choices=[("private", "Private"), ("public", "Public")],
default="private",
when=lambda answers: bool(answers.get("create_github_repo")),
),
]
Defaults, parsers, and validators
Defaults can be static values or callables. Use default="" for a text prompt that should accept
a blank answer.
from sprout import Question
questions = [
Question(
key="package_name",
prompt="Package name",
default=lambda answers: str(answers["project_name"]).replace("-", "_"),
),
]
Validators return (valid, message):
from sprout import Question, validate_repository_url
questions = [
Question(
key="repository_url",
prompt="Repository URL",
validators=[validate_repository_url],
),
]
Sprout includes validators for repository URLs, GitHub repository URLs, repository names, npm package names, and semantic versions.
Destination-aware questions
When question definitions need runtime context, make questions callable:
from pathlib import Path
from jinja2 import Environment
from sprout import Question
def questions(env: Environment, destination: Path) -> list[Question]:
return [
Question(
key="project_name",
prompt="Project name",
default=destination.name,
),
]
The callable must accept exactly two positional parameters: env and destination.
Rendering
Default rendering uses template_dir, or template when no directory is declared.
template_dir = "template"
Skipping files
should_skip_file receives a path relative to template_dir and the final answers:
from sprout import NO_LICENSE
def should_skip_file(relative_path: str, answers: dict[str, object]) -> bool:
return relative_path == "LICENSE.jinja" and answers.get("license") == NO_LICENSE
Jinja environment
Set Jinja2 extension classes with extensions:
from sprout import CurrentYearExtension, GitDefaultsExtension
extensions = [GitDefaultsExtension, CurrentYearExtension]
When extensions is omitted, the default environment includes Git defaults:
default_branchgit_user_namegit_user_emailgithub_username
Include GitDefaultsExtension explicitly when you provide a custom extension list and still want
those globals.
CurrentYearExtension exposes:
current_year
Prompt title and style
title = "Generate a Python package"
from sprout import ManifestContext
def title(context: ManifestContext) -> str | None:
return f"Generate project in {context.destination}"
The title is evaluated before answers are collected. For prompt appearance, assign style to a
sprout.CompactStyle (the default) or sprout.RailStyle instance.
Custom generation
Most templates should use the default renderer, but you can define apply(context) when generation needs
custom file creation, post-processing, or post-generation actions.
from sprout import ManifestContext, render_templates
def apply(context: ManifestContext):
return render_templates(
context.env,
context.template_dir,
context.destination,
context.answers,
render_paths=True,
)
apply must accept exactly one context parameter. It may return None, one path, or a sequence of
paths for the generated-files summary.
Programmatic APIs raise SproutError subclasses for expected operational failures. Catch the
specific error when recovery differs by failure type, or catch SproutError at an application
boundary. The sprout CLI translates these errors into concise process-exit messages.
Examples
License
Metadata
Release files for sprout-template 1.7.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| sprout_template-1.7.0.tar.gz | 55.4 kB | Without provenance | Details for sprout_template-1.7.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| sprout_template-1.7.0-py3-none-any.whl | Python 3 | none | any | Details for sprout_template-1.7.0-py3-none-any.whl |
Total release size: 121.7 kB
Release files / sprout_template-1.7.0.tar.gz
| Download URL | sprout_template-1.7.0.tar.gz |
|---|---|
| Size | 55.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
d5550aa7e1c0bec1dbae7001850fc6b8ec5bdc26870e63eb8be9836d034d8772
|
|
BLAKE2b-256 checksum How to use checksums |
1156f43111fb836169bd310f5ad9c8501a579e26a57ad91bd1743e49920a17b0
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.18
|
Release files / sprout_template-1.7.0-py3-none-any.whl
| Download URL | sprout_template-1.7.0-py3-none-any.whl |
|---|---|
| Size | 66.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
77340ea9c111336f6bd0eab272a87270126139c2148c5d70bd9c98240b3e8f7d
|
|
BLAKE2b-256 checksum How to use checksums |
0c08e17e7340b9a678bd75b52992b66979ca2ef1907fd96a2aa884ef56cd2fbd
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.18
|
Security for sprout-template 1.7.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.