Skip to main content

arch-blueprint

Explore the architecture of a Python project in the browser: its import graph, live, folded down to the packages you care about and opened as deep as you need. The same graph is drawn as PlantUML or D2 source or a PNG, for docs and CI, and compared across a branch or the project's history.

arch-blueprint serve on the example project examples/shop

The example project examples/shop, opened with arch-blueprint serve examples/shop.

  • Modules, or classes and functions. Each box is a module, an arrow an import between them. Switch to classes & functions to see which definition uses which.

  • Fold and open anything, independently. Every package — and, at the class level, every module — folds into one box or opens, on its own: one package opened down to its classes while its neighbours stay single boxes. A folded box counts all the imports inside it.

  • Follow a dependency. Click a box for what it depends on and what depends on it, an arrow for the imports behind it. A red arrow is a cycle.

  • Keep what you found. The view lives in the page's address; "⧉ CLI command" copies the draw command that renders it as a diagram.

  • Quick start

  • Explore in the browser: serve

  • Diagrams as text and images: draw, diff, history

  • Choosing what to draw

  • Metrics

  • Troubleshooting

  • Examples

Quick start

pip install arch-blueprint

Point it at the directory your packages sit in: src for a src layout, otherwise usually the repository root.

arch-blueprint serve src                # explore it in the browser

From a checkout of this repository, arch-blueprint serve examples/shop opens the example above.

The same graph as a diagram:

arch-blueprint draw src                 # PlantUML source on stdout
arch-blueprint draw src -o arch.puml    # the same, into a file
arch-blueprint draw src -o arch.png     # an image (needs `plantuml` on PATH)
arch-blueprint draw src -o arch.d2      # D2 source

Example project graph

In a diagram, arrows are drawn between packages, at the level where the two modules' paths part (app2.service → app1.models is drawn as app2 → app1); boxes can be classes and functions too (--links class).

To view a .puml without installing anything, paste it into the PlantUML web server. For .d2 there's the D2 playground. Or install the tool once (brew install plantuml / brew install d2) and ask for a .png directly.

With no -m, every package in the directory is drawn whole, and the tool says which ones it picked:

$ arch-blueprint draw examples/project_root -o example.png
arch-blueprint: drawing app1, app2, plugins (all modules; narrow with -m)

arch-blueprint --help lists the commands, and arch-blueprint COMMAND --help shows each command's options with examples.

Choosing what to draw

-m PATTERN chooses the modules. Repeat it to draw several packages together, with the links between them:

Pattern Selects
myapp.* the modules and subpackages directly inside myapp
myapp.** everything under myapp, at any depth
myapp.*.models models in every direct subpackage of myapp
myapp myapp itself, drawn as one box (the tool warns you)
arch-blueprint draw src -m 'myapp.*'
arch-blueprint draw . -m 'app1.*' -m 'app2.*' -m 'plugins.**'

Quote the patterns, or the shell expands the * itself.

A link is drawn only when both of its ends are selected. That includes a dependency on a package whose children you selected: pkg.* never selects pkg itself, but an import of pkg still counts.

Everything next to the selection

--deps out turns the selection into the focus and draws what it depends on too, wherever in PROJECT_DIR that lives — another package, another top-level package — without adding it to -m. --deps in draws the other side: what depends on the focus. Either way such a neighbor is drawn greyed out and dashed, under its full name, with every arrow between it and the focus, in both directions — a dependency that imports the focus back is drawn as the cycle it is. What the neighbors import from one another or from anything else is left out, and so are the standard library and third-party packages. It works at every --links level: the imported module (or its namespace), or the class or function a name refers to, followed through re-exports. At the class levels --deps in also parses every module importing the focus, directly or through others.

arch-blueprint draw src -m 'app.features.core.executory_processes.**' --deps out --links class
arch-blueprint draw src -m 'app.features.core.executory_processes.**' --deps in --links class

diff --base and history take --deps too; a snapshot records it, so two snapshots built with and without it can't be diffed. Metrics are not drawn with --deps (--metric is refused): the neighbors' own imports are not read, so their numbers would be wrong.

Commands

Command What it does
serve PROJECT_DIR opens a page in the browser to explore the graph interactively
draw PROJECT_DIR draws the project's import graph
draw SNAPSHOT draws a snapshot saved earlier by draw -o graph.json
diff draws what changed between two snapshots or two git revisions
history PROJECT_DIR writes one picture per commit that changed the graph

Output goes to stdout unless -o FILE is given. Without -f, the extension of -o picks the format:

-f -o extension Output
puml (default) .puml PlantUML source
d2 .d2 D2 source
puml-png .png image drawn by plantuml from PATH
d2-png — image drawn by d2 from PATH
json (draw only) .json a snapshot, to draw or diff later

A missing plantuml / d2 is reported before any work starts.

serve

serve opens a page in the browser where the graph is explored instead of chosen up front: no -m or --links to get right before you see anything.

arch-blueprint serve src                     # every package in src/
arch-blueprint serve src -m 'myapp.**'       # one package
arch-blueprint serve src --port 8000 --no-open
  • Modules, or classes and functions. "boxes" switches what a box is: a module, or a class or module-level function, with arrows wherever one's code uses another.
  • Fold and unfold, each on its own. The page opens on each package's direct subpackages, folded into one box each. Double-click a folded box to unfold it, or a frame to fold it back. Every package folds independently of the others, and at the class level so does every module: one package can be open down to its classes while its neighbours stay single boxes. A folded box draws what selecting that package whole (-m pkg) draws: its fan-in, fan-out and arrows count everything inside it.
  • Packages and modules. A folded package is a square box with a double border, a 📁 and the number of modules it holds; an unfolded one is a 📂 frame; a module is a plain rounded box.
  • Show only one package. "show only this" (in the details panel, or "only" beside a package in the tree, on hover) narrows the graph to that package's contents — its modules and subpackages, nothing around it. The browser's Back button, or "show all", returns.
  • Focus. Click a box: what it depends on and what depends on it stay lit, the rest fades. The panel on the right lists both, with the box's metrics. Esc clears the focus.
  • Arrows. Click an arrow to list the imports behind it. A red two-headed arrow is a cycle.
  • Large views. Switching to classes & functions keeps the folds, so definitions appear only where you opened. A view of more than 100 boxes is not drawn until you confirm: it is past reading, and laying it out can freeze the page.
  • Find. Type in the search box to highlight matching boxes. Enter goes to the first match, unfolding whatever package hides it.
  • Choose. The checkboxes on the left show what is drawn: a box on the graph is ticked, a module hidden inside a folded box is not (tick it to unfold down to it), and an opened package is half-ticked when only part of it is drawn. "all" and "none" tick or clear every box. Turn "auto-redraw" off to tick a few boxes without a redraw after each one, then press "Redraw". A package you tick is drawn as one box (double-click it to open it) — unless something inside it is already shown: then it opens one level, keeping that in view. "metric" shows a metric on every box or arrow.
  • Share and keep. The view lives in the page's address, so a link opens the same view. "⧉ CLI command" copies the draw command for the same modules, arrows and metric, ready for a doc or CI. draw folds nothing, so a view with folded packages notes that the command draws them unfolded.

The server listens on 127.0.0.1 only and stops with Ctrl-C. The import graph is read once per selection and kept while the server runs: restart serve to see edits to the code.

draw

arch-blueprint draw src                                   # every package in src/
arch-blueprint draw src -m 'myapp.*' -o arch.png          # one package, as an image
arch-blueprint draw src --metric fan_in --metric fan_out  # with metrics on the boxes
arch-blueprint draw src --cycle-details                   # with the imports behind each cycle

A cycle is drawn as a red double arrow. --cycle-details adds a note listing the imports that make it up, so you can see which ones to break. It is off by default in every command, because on a whole diagram the notes take up most of the picture:

arch-blueprint draw tests/fixtures/cyclic -m 'pkg_a.*' -m 'pkg_b.*' --cycle-details
pkg_a <-[#C0392B,bold]-> pkg_b
note on link
  **pkg_a -> pkg_b:**
  - core → util
  - services → util
  **pkg_b -> pkg_a:**
  - util → core
end note

A cycle isn't only a pair importing each other. A ring a → b → c → a of any length is found too and its arrows are drawn red; with --cycle-details it gets one note listing every import on it, tied to each of its modules. A pair inside such a ring gets no note of its own, so each import is listed once. A cycle can also run through a package's __init__.py: api.handlers imports services, whose __init__.py imports services.engine, which imports api.handlers. That __init__.py import closes the cycle but isn't drawn (every package's re-exports would bury the diagram); the note lists it as "package facade import, not drawn".

A namespace that is itself a module (writer importing storage.backend) stays a plain box. Packages without an __init__.py (PEP 420 namespace packages) work as well.

Arrows between packages or between modules

By default an arrow joins the packages where two modules' paths part: app2.service importing app1.models is drawn app2 ---> app1, with each module inside its package's frame (a chain of frames that hold nothing but one frame is merged into one, app.features.core). --links module draws it module to module instead:

arch-blueprint draw examples/project_root -m 'app1.*' -m 'app2.*' -m 'plugins.**' --links module
app2.service ---> app1.models
app2.service ---> plugins.auth.backend

Every arrow then ends on a box, so the boxes are drawn flat, without package frames, each labelled by its dotted name less the prefix all boxes share, which is shown once as the diagram title. An import that lands inside a selected package ends on that package's box; an import of a package itself (its __init__.py) gets a box of its own. Cycles, edge_weight and diff then work between modules. --links is given to the commands that build a graph: draw PROJECT_DIR, diff --base and history.

Arrows between classes and functions

--links class makes every top-level class and module-level function of the selected modules a box, and draws an arrow from each to every other one its code names, anywhere in it: bases, metaclass, decorators and class-level annotations, __init__, method signatures and method bodies, a function's parameters, their defaults, return annotation and body, string (forward) annotations and cast("Foo", x) included. Imports are followed through aliases, relative imports, imports inside a function and re-exports in a package's __init__.py, and a module-level variable through its value (Alias = Foo | Bar, T = TypeVar("T", bound=Foo), HANDLERS = {"a": Foo}, settings = Settings()); a parameter, local or match capture that happens to share a class's name isn't that class. Functions are boxes of their own, so classes that only a function ties together are linked through it:

arch-blueprint draw examples/project_root -m 'app1.*' -m 'app2.*' -m 'plugins.**' --links class
app2.service.make_user ---> app1.models.User
app2.service.make_user ---> plugins.auth.backend.AuthBackend

class draws the boxes flat under their dotted names, less the prefix every box shares (shown once, as the title); --links class-grouped draws each box inside the frame of its module instead, a chain of frames that hold nothing but one frame merged into one (app.features.core). Cycles, metrics, snapshots, diff and history work between classes and functions as they do between modules. A pair of boxes is one import however many times one names the other, so edge_weight is always 1. Nothing is imported or run — the source is read with libcst, which takes some ten times longer than drawing modules.

Snapshots

draw -o graph.json (or -f json) saves a snapshot instead of a picture: the modules, the imports between them and every metric. Give draw the snapshot instead of a directory and it draws it, in any format and with any metrics, byte for byte as it would have drawn the project. The project isn't needed any more, and diff compares two snapshots:

arch-blueprint draw src -m 'myapp.*' -o graph.json
arch-blueprint draw graph.json -o graph.png --metric instability

A snapshot records its --links level and --deps, so drawing or diffing one takes neither, and two snapshots built at different levels (or with and without --deps) can't be diffed.

diff

diff draws the graph with the changes marked on it. It can compare two snapshots:

arch-blueprint diff old.json new.json -o diff.png

Or, when the project is in git, a revision against the working tree (or against --head REV). No stored files are needed:

arch-blueprint diff --base origin/main src -o diff.png
arch-blueprint diff --base v1.0 --head v2.0 src -m 'myapp.*'

Diff: a module and link added, a module and link removed

Unchanged parts are drawn as on a plain diagram. Every change is dashed and has a color and a label of its own:

Marker Module Dependency
added green, spot +, «added», dashed frame green dashed arrow, added
removed red, spot -, «removed», dashed frame red dashed arrow, removed
new cycle — red dashed <->, NEW CYCLE
cycle resolved — grey dashed arrow, cycle resolved, in the direction that remains (a bare line if neither does)

A longer cycle is marked on its arrows the same way: red dashed NEW CYCLE on the unchanged arrows of a new one (the import that closed it is added), grey dashed cycle resolved on what's left of one that went.

  • --changes-only draws just the changes and the modules they touch. That helps on a large project.
  • --cycle-details adds the notes that list the imports of each new cycle.
  • A package that exists on one side only is shown as added or removed; it doesn't fail the run.
  • A module replaced by a package of the same name (api.py → api/) is drawn inside that package (with --links module, beside it, as api (module)).
  • When nothing changed, you still get a valid diagram, with "No architectural changes" in the legend.
  • --metric NAME draws a metric as a plain diagram does, and shows how the changes moved it (see Metrics in a diff).

diff exits like diff(1): 0 when nothing changed, 1 when something did, 2 on an error. With --metric, a shown metric whose value changed counts as a change, even when the structure didn't; without it, only structure counts. The diagram is written either way. In CI, to fail on errors but not on changes:

arch-blueprint diff --base origin/main src -o diff.png || test $? -eq 1

history

history walks the branch's first-parent history (one commit per merged pull request) and draws one picture per commit that changed the graph. The first frame is the plain diagram. Every later frame is the same diagram with what changed since the previous frame marked on it, like diff (--changes-only for just the changes). Everything is declared in the plain diagram's order, so consecutive frames lay out alike. Commits that leave the graph alone are skipped.

With --metric, every frame shows the metrics, each changed value as old → new like diff. A commit that moves a shown metric is then a frame too, even with no structural change: one more import inside an existing arrow changes edge_weight, so the last frame's values are the ones at --head. Without --metric, only structure makes a frame.

arch-blueprint history src                                         # every package in src/
arch-blueprint history src myapp -f puml-png                       # one package, as images
arch-blueprint history src app1 app2 -m 'app1.*' -m 'app2.core.*'  # packages narrowed by -m
arch-blueprint history src myapp --base v1.0 --head main -o album
blueprint-history/               (or -o DIR)
  0001_2026-05-02_ab12cd3.png    the first frame: the plain diagram
  0002_2026-05-12_ef45ab6.png    the diagram at that commit, its changes marked
  index.md                       the frames in order, with dates and commit subjects
  • ROOTs. The packages named after PROJECT_DIR are the ones drawn, each whole unless -m narrows it. With none named, it's every package the project has at --head. A package that a commit doesn't have yet is no error: the commit is reported as no source yet, and the package shows up in the first frame that has its code.
  • One kind of file. An album holds either sources (-f puml, -f d2) or images (-f puml-png, -f d2-png).
  • Caching. Snapshots and images are cached in ./.arch-blueprint (--cache-dir). After a failed image, rerunning redraws only what is missing, and another album of the same history reuses the cache.
  • Large diagrams. d2 refuses to rasterize very large diagrams. Such a diagram is redrawn at half the scale, then at half again; --scale FACTOR sets the starting scale. PlantUML's size limit is raised to 16384 px (PLANTUML_LIMIT_SIZE; a value you set yourself wins).
  • Reruns. Unchanged files are not rewritten. Frame files from an earlier run that this run didn't produce are removed; nothing else in the directory is touched.
  • Broken commits. A commit whose code can't be analyzed is reported as skipped, with the reason.

Metrics

--metric NAME shows a metric; repeat it for more. arch-blueprint --list-metrics prints this list:

Metric On Meaning
fan_in each box how many modules depend on this one
fan_out each box how many modules this one depends on
instability each box fan_out / (fan_in + fan_out): 0 = only depended upon, 1 = only depends on others
edge_weight each arrow how many module imports the arrow stands for

The metrics appear in the order you ask for them. A cycle is one arrow standing for two links, so edge_weight shows both values there, as forward/backward.

arch-blueprint draw tests/fixtures/cyclic -m 'pkg_a.*' -m 'pkg_b.*' --cycle-details \
  --metric fan_in --metric fan_out --metric instability --metric edge_weight

Metrics on nodes and on a cyclic connection

pkg_b.util is depended on twice and depends on one module, so instability: 0.33. The arrow is a cycle, so edge_weight reads 2/1: two imports one way and one the other.

Metrics in a diff

diff and history take --metric too. A value that is the same on both sides is drawn as on a plain diagram. One that changed reads old → new, with the difference for a number:

class pkg_b.util <<(M, #2ECC71)>> {
  fan_in: 2
  fan_out: 1 → 2 (+1)
  instability: 0.33 → 0.5 (+0.17)
}
pkg_a <-[#C0392B,bold]-> pkg_b : edge_weight=2/1 → 2/2

An added module or arrow shows its new value, a removed one its old value. A cycle that appeared reads, say, NEW CYCLE edge_weight=2 → 2/1 (one direction before, both after); a resolved one is drawn as the direction that remains, cycle resolved edge_weight=2/1 → 2. With --changes-only, the modules and arrows whose shown values moved are drawn too. The legend adds metric: old → new (difference).

New metrics are self-contained plugins under src/arch_blueprint/metrics/. See CLAUDE.md for the protocols.

Troubleshooting

Errors are one line on stderr and exit 2 (1 when an analysis or an image could not finish; diff uses its own codes, see above). Nothing fails silently: a pattern that matches no module is an error, not an empty diagram. Common mistakes:

Message What to do
a command comes first now — run: arch-blueprint draw … Earlier versions had no command word (arch-blueprint DIR -m …). Put draw first.
'render' is part of 'draw' now arch-blueprint draw graph.json … draws a snapshot.
src is itself a package — pass the directory that contains it PROJECT_DIR is the directory the packages sit in, not a package.
. keeps its packages in src A src layout: run arch-blueprint draw src.
no package 'myap' in src (found: myapp, tests) A typo in -m; the message lists the packages that exist.
no modules matched: 'myapp.zzz.*' The package exists but the pattern selects nothing inside it.
-m 'myapp' draws the package as a single box Use myapp.* or myapp.**.
-m took 'src' as a pattern -m takes every value up to the next option: put PROJECT_DIR first, or give one pattern per -m.
-f puml-png needs 'plantuml' on PATH Install PlantUML / D2, or write source (-o x.puml) and view it online.
unknown metric 'fanin'. Available: … See arch-blueprint --list-metrics.
--links applies when building a graph; a snapshot already records its link level Pass --links when saving the snapshot (draw DIR --links module -o graph.json), not when drawing it.
cannot diff a namespace-level snapshot against a module-level one Save both snapshots with the same --links.
--metric does not go with --deps Draw metrics in a run without --deps.
cannot diff a snapshot built without --deps against one built with --deps out Save both snapshots with the same --deps.
warning: cannot parse '…/x.py' (…); its definitions are left out. With --links class / class-grouped, a file that isn't valid Python; the rest is drawn.

A pattern resolves against PROJECT_DIR first, then against the installed packages. Graphing arch_blueprint itself always resolves to the running copy.

Examples

Generated with this tool from released packages, so you can reproduce them:

pip install --target /tmp/pkgs wemake-python-styleguide fastapi taskiq
arch-blueprint draw /tmp/pkgs -m 'wemake_python_styleguide.*'

wemake-python-styleguide

wemake-python-styleguide 1.8.0: 14 modules, 29 links, no cycles. Source: docs/images/wemake.puml

This is what a layered codebase looks like when nothing points back up. checker sits alone at the top, everything drains toward types, constants and compat at the bottom, and no arrow is red.

wemake-python-styleguide module graph

FastAPI

fastapi 0.141.1: 27 modules, 65 links, 4 cycles. Source: docs/images/fastapi.puml

FastAPI module graph

Taskiq

taskiq 0.12.5: 31 modules, 87 links, 6 cycles. Source: docs/images/taskiq.puml

Taskiq module graph

With metrics

arch-blueprint draw /tmp/pkgs -m 'taskiq.*' \
  --metric fan_in --metric fan_out --metric instability

taskiq.abc reads fan_out: 13, instability: 1.0: it depends on thirteen modules and nothing depends on it, which is what an abstract-base module should look like. taskiq.compat is the opposite, at fan_in: 8, instability: 0.0.

Source: docs/images/taskiq_metrics.puml

Taskiq module graph with metrics

Development

This project uses uv.

uv sync                       # install deps
uv run pytest                 # run the test suite
uv run pre-commit run -a      # lint, format, type-check, test (what CI runs)

License

MIT — see LICENSE.

Metadata

Release files for arch-blueprint 0.3.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for arch-blueprint 0.3.0
File Size Uploaded
arch_blueprint-0.3.0.tar.gz 2.9 MB Details

Built distribution (wheel)

Table of built distributions (wheels) for arch-blueprint 0.3.0
File Interpreter ABI Platform
arch_blueprint-0.3.0-py3-none-any.whl Python 3 none any Details

Total release size: 3.6 MB

Release files / arch_blueprint-0.3.0.tar.gz

Download URL arch_blueprint-0.3.0.tar.gz
Size 2.9 MB
Tags Source
SHA-256 checksum
How to use checksums
e1ba08d3b345b13dcbad2a829b478c7e73dc97a5fe7a3e552b18e582f62da932
BLAKE2b-256 checksum
How to use checksums
97bbf66ae3c83c6f6f490bc999891c3b4a402b361e8b33a2344b92e447c75886
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

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.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 11, 2026.

Transparency log

Release files / arch_blueprint-0.3.0-py3-none-any.whl

Download URL arch_blueprint-0.3.0-py3-none-any.whl
Size 729.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
3291acb240e4e842b16e6161d1b03480daf9668d79e1fd48d63e55abe0417d74
BLAKE2b-256 checksum
How to use checksums
ff6954e2fd89491eb3757d9864ad01cf8a48f57a0c8c7efd292b78acf293215b
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

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.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 11, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.3.0 This release

2 release files

0.2.1

2 release files

0.2.0

2 release files

0.1.6

2 release files

0.1.5

2 release files

0.1.4

2 release files

0.1.3

2 release files

0.1.2

2 release files

0.1.1

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page