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.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file arbol_tree-0.2.0.tar.gz.
File metadata
- Download URL: arbol_tree-0.2.0.tar.gz
- Upload date:
- Size: 13.2 kB
- Tags: Source
- Uploaded using 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}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f5bc86df972cfaf6953bc71582e10c55007d496c735b874fd5ae485863853909
|
|
| MD5 |
d4d4787ee24f54f7fae34d6413f7fc5d
|
|
| BLAKE2b-256 |
d6a93c40ff4f5ff70da2ae722f34ca4d58c397419c843b4d014cdd4fd8d55512
|
File details
Details for the file arbol_tree-0.2.0-py3-none-any.whl.
File metadata
- Download URL: arbol_tree-0.2.0-py3-none-any.whl
- Upload date:
- Size: 15.5 kB
- Tags: Python 3
- Uploaded using 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}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0734817e66265eb782bc7ae34cfc85eaa7a821fc0cf9ea2b1076d2337dfa93cc
|
|
| MD5 |
785d1a239c852f28d30d9ec2f59f7a30
|
|
| BLAKE2b-256 |
bf290c93a5355f0670f725d76333c1e33b7a5de8ba8da7b1b3f708c1943f74ae
|