Skip to main content

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 --from is required, and it is not optional boilerplate. A bare uvx arbol resolves the unrelated arbol package on PyPI and fails with Package 'arbol' does not provide any executables. uvx assumes 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 as tree behaves without -a
  • dunder folders__pycache__/, __snapshots__/ — build residue rather than content. Dunder files like __init__.py are 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 arbol when passing a wildcard. The installed arbol.exe expands wildcards before Python runs, so -I '*.pyc' arrives as the list of files it matched and the command fails with Got unexpected extra argument(s). Quoting does not help; the expansion happens below the shell. Plain arbol . 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
  • ROOT records the real directory name, not the path you typed — arbol . -o tree.json displays . 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. tree behaves 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_patterns matches 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 -a is given. -I patterns 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

arbol_tree-0.2.0.tar.gz (13.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

arbol_tree-0.2.0-py3-none-any.whl (15.5 kB view details)

Uploaded Python 3

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

Hashes for arbol_tree-0.2.0.tar.gz
Algorithm Hash digest
SHA256 f5bc86df972cfaf6953bc71582e10c55007d496c735b874fd5ae485863853909
MD5 d4d4787ee24f54f7fae34d6413f7fc5d
BLAKE2b-256 d6a93c40ff4f5ff70da2ae722f34ca4d58c397419c843b4d014cdd4fd8d55512

See more details on using hashes here.

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

Hashes for arbol_tree-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 0734817e66265eb782bc7ae34cfc85eaa7a821fc0cf9ea2b1076d2337dfa93cc
MD5 785d1a239c852f28d30d9ec2f59f7a30
BLAKE2b-256 bf290c93a5355f0670f725d76333c1e33b7a5de8ba8da7b1b3f708c1943f74ae

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page