Skip to main content

arch-blueprint

Draws the import graph of a Python project as an architecture diagram, in PlantUML or D2 source or as a PNG. It also shows what a branch changed in that graph, and how the graph grew commit by commit.

Example project graph

Each box is a module. An arrow means a module on one side imports a module on the other. Arrows are drawn between packages, at the level where the two modules' paths part (app2.service → app1.models is drawn as app2 → app1). A red double arrow is a cycle: two packages that import each other.

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 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

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.

Commands

Command What it does
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.

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. --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, each under its full name, without package frames. 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.

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, so drawing or diffing one takes no --links, and two snapshots built at different levels 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.

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.2.1

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.2.1
File Size Uploaded
arch_blueprint-0.2.1.tar.gz 1.8 MB Details

Built distribution (wheel)

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

Total release size: 1.9 MB

Release files / arch_blueprint-0.2.1.tar.gz

Download URL arch_blueprint-0.2.1.tar.gz
Size 1.8 MB
Tags Source
SHA-256 checksum
How to use checksums
d7e98a33ec2da7c29a6b8908f20cb034a5309c06de456d75c24279ced5f6dd47
BLAKE2b-256 checksum
How to use checksums
dc1ce374e4e606a988060af2b5d2e2ac22fa498c39e21bd8750774a1dc9c1cb9
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 Sep 27, 2026.

Transparency log

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

Download URL arch_blueprint-0.2.1-py3-none-any.whl
Size 88.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
d2a9314491f013d06593514dce71f67687515e1fb9fba3bee98fdb266021e6ef
BLAKE2b-256 checksum
How to use checksums
bdffeb7deb4edca933f71615caa2e3cd7a166e73410f8b879a61372e7a498b21
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 Sep 27, 2026.

Transparency log

Release history Release notifications | RSS feed

0.3.0

2 release files

This release

0.2.1 This release

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