Arbol
Draw tree structures in the terminal, in the style of the Linux tree command
— on Windows, macOS and Linux, with the same commands everywhere.
$ arbol tests/data/sample_tree -I 'datasets|assets|suite|scripts' -L 3
tests/data/sample_tree
├── docs
│ ├── images
│ │ ├── diagram.svg
│ │ └── logo.png
│ ├── api.md
│ ├── changelog.md
│ └── guide.md
├── src
│ ├── core
│ │ ├── utils
│ │ ├── engine.py
│ │ └── parser.py
│ ├── plugins
│ │ ├── export.py
│ │ └── importer.py
│ └── __init__.py
├── vendor
│ ├── bundle.min.css
│ └── legacy.js
├── config.yaml
└── README.md
Hidden entries are skipped by default — no .hidden/, no .env.example, no
__pycache__/. Pass -a to see them. Directories sort before files, then
case-insensitively.
What it's for
Four uses, one renderer behind all of them.
1. tree, the same on every platform
tree is a Linux tool. Getting it on Windows means Chocolatey, WSL, or Git
Bash, and the flags differ once you do. Arbol is a Python package, so it
installs the same way everywhere and takes the same commands everywhere:
arbol .
arbol . -a -L 2
Output matches GNU tree — with -a, byte-identical to tree -a --dirsfirst
apart from the summary footer. Flag names follow tree's, so -L, -I and
-a mean what you already expect.
2. Save the structure, edit it, draw it again
-o writes the walk to JSON. Edit that file however you like, then point arbol
back at it:
arbol . -o tree.json
arbol tree.json
Useful for proposing a layout before building it — add the directories you plan to add, delete what you plan to remove, and render the result. The edited file goes through exactly the same renderer as a real walk, so what you see is what a finished tree would look like.
3. Draw a tree you wrote yourself
The JSON needs no filesystem behind it. Any nesting of objects, arrays and strings draws, so you can describe an org, a schema, a taxonomy — anything tree-shaped:
{
"ROOT": "Acme Corp",
"Engineering": {"Backend": ["api", "workers"], "Frontend": ["web", "mobile"]},
"Operations": {"Support": "tier-1"}
}
$ arbol acme.json
Acme Corp
├── Engineering
│ ├── Backend
│ │ ├── api
│ │ └── workers
│ └── Frontend
│ ├── web
│ └── mobile
└── Operations
└── Support
└── tier-1
4. Draw a Python dict directly
No file, no CLI — import it and pass the dict:
import arbol
arbol.print_tree(
{
"ROOT": "Acme Corp",
"Engineering": {"Backend": ["api", "workers"], "Frontend": ["web", "mobile"]},
"Operations": {"Support": "tier-1"},
}
)
Same output as above. Use arbol.render(...) instead to get it back as a
string — useful for logs, tests, or writing into a report.
Installing
The distribution is named arbol-tree, since arbol on PyPI is an
unrelated package. The import name and the command are both arbol.
pip install arbol-tree
As a uv tool
To get an arbol command on your PATH without putting it in any project's
environment:
uv tool install arbol-tree
arbol .
Upgrade or remove it later with uv tool upgrade arbol-tree and
uv tool uninstall arbol-tree.
Without installing anything
uvx fetches, runs, and discards in one step — handy for a one-off, or for
trying it before committing to an install:
uvx --from arbol-tree arbol .
The
--fromis required, and it is not optional boilerplate. A bareuvx arbolresolves the unrelatedarbolpackage on PyPI and fails withPackage 'arbol' does not provide any executables.uvxassumes the package name matches the command name; here it does not.
Pin a version the same way:
uvx --from arbol-tree==0.1.0 arbol .
From a checkout
uv run arbol .
Command line
arbol [OPTIONS] [PATH]
PATH is a directory to walk or a JSON file to read, and defaults to the
current directory. Arbol works out which it is; there is no mode flag.
| Option | Description |
|---|---|
-a, --all |
Show hidden entries: dot names, and dunder folders |
-L, --level |
Maximum display depth, counting levels below the root |
-I, --ignore |
Wildcard pattern to skip, at any depth. Repeatable |
-o, --output |
Save the intermediate JSON here instead of a temporary file |
-V, --version |
Show the version and exit |
What is hidden by default
Two rules, so the common case needs no flags at all:
- anything starting with a dot —
.git/,.venv/,.gitignore,.env— files and folders alike, exactly astreebehaves without-a - dunder folders —
__pycache__/,__snapshots__/— build residue rather than content. Dunder files like__init__.pyare source, and stay
tree does not have that second rule; it is the one place arbol's default
deliberately goes further. -a turns both off:
arbol . -a
With -a, arbol's output is byte-identical to tree -a --dirsfirst apart from
the summary footer.
Ignore patterns
-I takes the same wildcard patterns as tree -I, matched against each
entry's name — never its path, so -I 'src/*.pyc' matches nothing, exactly
as in tree.
| Operator | Matches |
|---|---|
* |
Zero or more characters |
? |
Any single character |
[abc], [a-z] |
One character from the set |
[^abc] |
One character not in the set |
| |
Either alternate |
trailing / |
Restricts the pattern to directories |
-I is for what the default does not already cover — build output, vendored
trees, anything noisy that is not hidden:
arbol . -I '*.pyc' -I node_modules
The trailing slash is the subtle one: it is the difference between hiding a folder and hiding everything that shares its naming convention. Ignoring a directory ignores everything beneath it.
-I is repeatable, and | does the same job inside a single pattern:
arbol . -I '*.pyc|node_modules|dist'
Matching is case-sensitive, as in tree without --ignore-case.
On Windows, use
python -m arbolwhen passing a wildcard. The installedarbol.exeexpands wildcards before Python runs, so-I '*.pyc'arrives as the list of files it matched and the command fails withGot unexpected extra argument(s). Quoting does not help; the expansion happens below the shell. Plainarbol .is unaffected — the default rule needs no pattern — and so are patterns without wildcards, like-I node_modules.
The walk options describe a walk, so passing them alongside a JSON file is an error rather than a silent no-op.
The JSON format
Both the saved-and-edited case and the hand-written case use one format —
objects, arrays and strings, nothing else. ROOT names the root node and is
not drawn as a branch. Everything else follows three rules: an object
contributes one branch per key, an array contributes one child per item, and
anything else becomes a leaf.
For a walked directory that means:
- a directory is a list of entries
- a file is a string in that list
- a subdirectory is a single-key dict,
{"name": [...]}
One quirk when hand-editing: top-level entries are keys, deeper ones are
not. A top-level file is written "README.md": [] — an empty list renders as
a leaf — while a nested file is a plain string inside its parent's list. Files
have two representations depending on depth.
Two things to know about saving:
- the file holds what was walked, so hidden entries are absent unless you
save with
-a ROOTrecords the real directory name, not the path you typed —arbol . -o tree.jsondisplays.but writes"ROOT": "myproject"
Python API
Two objects, kept deliberately apart. JsonBuilder decides what is in the
tree; ArbolTerminalView draws exactly what it is given and filters nothing.
import arbol
builder = arbol.JsonBuilder(ignore_patterns=["__*__/", "*.pyc"], level=2)
json_path = builder.write_directory("some/dir")
view = arbol.ArbolTerminalView()
print(view.render(arbol.JsonBuilder.load(json_path)))
Module-level shortcuts cover the simple cases:
arbol.render({"ROOT": "r", "a": ["x", "y"]})
arbol.print_tree(arbol.load_json("tree.json"))
arbol.write_directory_json("some/dir", level=2)
By default write_directory puts the JSON in a temporary folder and leaves it
there for you to clean up. Pass output_path to save it somewhere real.
Behavior worth knowing
- Symlinked directories are listed but never followed, so a symlink loop cannot
blow up the output.
treebehaves the same without-l. - A directory that cannot be read is drawn as empty rather than aborting the walk.
- Entries sort directories first, then files, case-insensitively — the same
order as
tree --dirsfirst. ignore_patternsmatches names at any depth, never paths, and applies to files and directories alike unless a trailing/narrows it.- Dot names and dunder folders are hidden unless
-ais given.-Ipatterns apply on top, and still apply with-a.
Development
uv sync
uv run pytest
uv run ruff check .
uv run ruff format .
Releasing to PyPI, including the TestPyPI rehearsal, is written up in RELEASING.md.
License
MIT. See LICENSE.
Metadata
Release files for arbol-tree 0.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 | |
|---|---|---|---|---|
| arbol_tree-0.2.0.tar.gz | 13.2 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| arbol_tree-0.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 28.7 kB
Release files / arbol_tree-0.2.0.tar.gz
| Download URL | arbol_tree-0.2.0.tar.gz |
|---|---|
| Size | 13.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
f5bc86df972cfaf6953bc71582e10c55007d496c735b874fd5ae485863853909
|
|
BLAKE2b-256 checksum How to use checksums |
d6a93c40ff4f5ff70da2ae722f34ca4d58c397419c843b4d014cdd4fd8d55512
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.1 {"installer":{"name":"uv","version":"0.12.1","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 / arbol_tree-0.2.0-py3-none-any.whl
| Download URL | arbol_tree-0.2.0-py3-none-any.whl |
|---|---|
| Size | 15.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
0734817e66265eb782bc7ae34cfc85eaa7a821fc0cf9ea2b1076d2337dfa93cc
|
|
BLAKE2b-256 checksum How to use checksums |
bf290c93a5355f0670f725d76333c1e33b7a5de8ba8da7b1b3f708c1943f74ae
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.1 {"installer":{"name":"uv","version":"0.12.1","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 arbol-tree 0.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.