python-dirtt — Directory Tree Templater
Dirtt generates directory and file structures from simple XML or JSON templates that describe repeatedly used filesystem layouts — project scaffolds, VFX show structures, or any tree you build more than once.
Write the layout once as a template, then stamp it out anywhere with variables filled in:
$ dirtt create -t project.xml --var project_root=/jobs --var project_path=commercial_spot
created tree: 81 actions
- Zero runtime dependencies — pure Python standard library
- Python 3.10+
- Templates in XML (the classic dirtt dialect, unchanged since 0.x) or JSON
--dry-runprints the full plan without touching the filesystem- Introspection: point dirtt at an existing tree and get a template back
(c) 2011–2026 Robert Moggach and contributors. Licensed under the MIT license.
Install
pip install python-dirtt
Command line
dirtt create -t TEMPLATE [--var KEY=VALUE ...] [--dest DIR] [--dry-run] [-i] [-w] [-v]
[--skip-existing-files] [--actor WHO] [--no-log]
dirtt list # show the packaged example templates
dirtt placeholders -t TEMPLATE # show the variables a template requires
dirtt introspect PATH [-o FILE]# generate a template from a real tree
dirtt migrate PATH --map MAP.json [--apply] [--prune]
create prompts interactively for any {{placeholder}} you don't pass
with --var. --dry-run prints each planned action (mkdir, write,
symlink) instead of performing it. -i/--interactive confirms each
directory; answering no skips that directory and everything inside it.
-w/--warn fails instead of continuing when a directory already exists.
--skip-existing-files leaves existing files and symlinks alone instead
of failing, which makes re-running a template over a tree that already
exists a clean no-op.
placeholders reports every variable the build needs, including those
used only inside xi:included templates and href'd content files, and
create prompts for that same full set.
After a successful create, dirtt appends one JSON line to
lib/dirtt/scaffold.log.jsonl under the created root recording what was
built, with what variables, when and by whom:
{"actor":"rm","date":"20260830","run":"project","template":"studio_project@2026.01","vars":{"project_code":"ABC"}}
The template id and run name are derived from the template's path and
can be pinned with --template-id and --run; --actor defaults to
$USER, and --no-log turns the log off. A log that cannot be written
warns and never fails the scaffold.
Library
from dirtt import build
# create the tree
build("project.xml", {"project_root": "/jobs", "project_path": "myproject"})
# or preview first
for action in build("project.xml", context, dry_run=True):
print(action.describe())
The stages are also available separately:
from dirtt import load_template, plan, execute, introspect
tree = load_template("project.xml", context) # frozen dataclass Tree
actions = plan(tree, dest="/somewhere/else") # ordered list[Action], absolute paths
execute(actions) # apply (or dry_run=True)
xml = introspect("/jobs/existing_project") # tree -> template
Errors raise dirtt.DirttError subclasses (TemplateError,
BuildError); the library never prints or exits.
XML templates
The dialect is unchanged from dirtt 0.x:
<?xml version="1.0" encoding="UTF-8"?>
<dirtt name="Project Tree" version="1.0"
dirname="{{project_root}}" basename="{{project_path}}"
username="pipeline" group="artist" perms="02755"
xmlns:xi="http://www.w3.org/2001/XInclude">
<dir basename="src" perms="02755">
<file basename="README.md" href="readme_snippet.md" perms="0644"/>
</dir>
<dir basename="renders" id="renders-dir"/>
<link basename="latest" idref="renders-dir"/>
<xi:include href="shared_structure.xml"/>
</dirtt>
dirtt— the root directory:dirname(parent path) +basename(directory name). Ifbasenameis omitted it is split offdirname.dir— a directory; nestsdir,file, andlinkelements.file— a file;hrefnames a content template (resolved next to the tree template, then in the packaged templates) rendered with the same{{variables}}; withouthrefthe file is created empty.link— a symlink;refis a literal target path,idrefpoints at theidof adirin the same tree. Links are created last.xi:include— splice another template's children in place;hrefmay be relative, absolute, or anhttp(s)://URL.permsis octal text ("02775");username/groupare applied withchownonly when running as root, and skipped otherwise.substitute="false"on afilecopies itshrefsource byte for byte, leaving any{{placeholders}}in it intact. Use it when the file being copied is itself a template.- An
idmust be unique across a template and everything it includes; a duplicate is an error rather than a silently mis-pointedidref.
JSON templates
The same schema as JSON — children are tagged with "type":
{
"name": "Project Tree",
"dirname": "{{project_root}}",
"basename": "{{project_path}}",
"perms": "02755",
"children": [
{ "type": "dir", "basename": "src", "children": [
{ "type": "file", "basename": "notes.txt", "content": "for {{project_path}}" }
]},
{ "type": "dir", "basename": "renders", "id": "renders-dir" },
{ "type": "link", "basename": "latest", "idref": "renders-dir" },
{ "include": "shared_structure.json" }
]
}
Files may carry inline "content" (JSON only) or an "href" like XML.
Migrating from 0.2.x
| 0.2.x | 1.0 |
|---|---|
mktree.py --template ... --interactive |
dirtt create -t ... -i |
mktemplate.py -p PATH |
dirtt introspect PATH |
mkproject.py |
dirtt create with your studio's template |
DirectoryTreeHandler(verbose, template, kwargs).run() |
build(template, kwargs) |
dirtt.util.template (Paste engine, eval-based) |
{{name}} placeholders only |
| Python 2, distutils | Python 3.10+, pyproject.toml |
Your existing XML templates work as-is. The old template engine's
{{if}}/{{for}}/{{py:}} constructs were never used by tree
templates and are no longer supported.
Migrating a tree between template versions
When a layout changes, dirtt migrate moves an existing tree onto the
new one rather than leaving you to mv by hand:
dirtt migrate /jobs/myproject --map migrations/2015_to_2026.json # dry run
dirtt migrate /jobs/myproject --map migrations/2015_to_2026.json --apply
Or copy into a new root instead of moving in place, which leaves the original project working and lets a bad map cost a directory rather than a production:
dirtt migrate /jobs/myproject --map MAP.json --copy --dest /volumes/new/myproject --apply
--copy also migrates across filesystems, which a move cannot do at all
(a rename between volumes fails with EXDEV). The same mapping file
drives both: the map describes the taxonomy translation, the flag
decides what happens to the bytes. With --copy the source is read-only
and orphans are left in it rather than carried into the new tree.
A mapping file pairs old paths with new ones, and may add prefix_rules
for whole subtrees. Matching is: explicit entry, then prefix rule, then
an identical path (unchanged paths need no mapping).
It is deliberately cautious, because it moves real data:
- Dry run is the default.
--applyis required to move anything. - Nothing is ever deleted. A path the mapping does not account for
is an orphan: reported and left where it is. Only
--pruneremoves anything, and only an orphaned directory that is already empty. - Case-only renames are staged through a temporary name, because macOS, SMB and Dropbox are case-insensitive and a direct rename there can lose the directory.
- It is idempotent. Re-running after a successful migration plans nothing, because every path now matches itself.
--copynever writes to the source, and--pruneis rejected with it, since nothing in the source is emptied.
Development
git clone https://github.com/rjmoggach/python-dirtt
cd python-dirtt
uv run --group dev pytest
Contributions welcome — code, tests, docs, bug reports, ideas.
Metadata
Release files for python-dirtt 1.2.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 |
|---|---|---|---|---|
| python_dirtt-1.2.0.tar.gz | 46.0 kB | Without provenance | Details for python_dirtt-1.2.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| python_dirtt-1.2.0-py3-none-any.whl | Python 3 | none | any | Details for python_dirtt-1.2.0-py3-none-any.whl |
Total release size: 83.8 kB
Release files / python_dirtt-1.2.0.tar.gz
| Download URL | python_dirtt-1.2.0.tar.gz |
|---|---|
| Size | 46.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
947006bb0614bd92bfc2a8522165ee407266f3eb964f52948607fc4246e67286
|
|
BLAKE2b-256 checksum How to use checksums |
504e34dbe6e5ffb249c598ab73ce8be64aed973cba1ac0abed6c8343b1f11f17
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.7 {"installer":{"name":"uv","version":"0.12.7","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / python_dirtt-1.2.0-py3-none-any.whl
| Download URL | python_dirtt-1.2.0-py3-none-any.whl |
|---|---|
| Size | 37.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
96b18630e2dfd0ce438ddc33316eb4514b6063cd63f353c6ec327e133c915cdc
|
|
BLAKE2b-256 checksum How to use checksums |
78fc06f9379db354cf4025bfa686c8fb8280aa7cf6e4a57f1d2f5c67afc2c32f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.7 {"installer":{"name":"uv","version":"0.12.7","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Security for python-dirtt 1.2.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.