Skip to main content

Description

Generate modules import graph for python project. Using plantuml for render.

Installation

pip install arch-blueprint

Usage

arch-blueprint --help
usage: arch-blueprint [-h] --modules [MODULES ...] [--format {puml,d2,json}]
                      [--metric NAME] [--no-cycle-details]
                      project_dir

Generate architecture diagrams for Python applications. Subcommands: 'render'
draws a snapshot, 'diff' compares two, 'history' draws one diagram per commit
that changed the graph.

positional arguments:
  project_dir           Path to root directory of target project

options:
  -h, --help            show this help message and exit
  --modules, -m [MODULES ...]
                        Selected modules for rendering (examples:
                        'myapp.somemodule', 'myapp.somemodule.*',
                        'myapp.*.*.models.*', 'myapp.somemodule.**')
  --format, -f {puml,d2,json}
                        Output format. Possible values: ['puml', 'd2', 'json']
  --metric NAME         Display a metric (repeatable). A node metric renders
                        as a block on each node (e.g. --metric fan_in); a link
                        metric renders as a label on each connection (e.g.
                        --metric edge_weight).
  --no-cycle-details    Hide detailed information for cyclic dependencies

A run against the bundled example project:

arch-blueprint examples/project_root -m 'app1.*' -m 'app2.*' -m 'plugins.**'

Example project graph

The PlantUML source behind it
@startuml
!theme amiga

top to bottom direction
hide empty members

package app1 {
  class app1.models <<(M, #2ECC71)>>
}
package app2 {
  class app2.service <<(M, #2ECC71)>>
}
package plugins {
  class plugins.auth.backend <<(M, #1ABC9C)>>
}

app2 ---> app1
app2 ---> plugins
@enduml

How the diagram is built

A node is a module. A link is aggregated to the namespace where two modules first differ, so app2.service importing app1.models is drawn as app2 ---> app1. Those endpoints are declared as package blocks with the modules inside, so the emitted source names everything it points at. (PlantUML would also infer the container from the dotted class names and render the same picture — declaring it is about the source saying what it means, not about fixing the image.) A namespace that is itself a module (writer importing storage.backend) stays a plain class: wrapping a class in a package of its own name is a syntax error.

-m is repeatable, which is how you graph sibling packages under a root that has no __init__.py of its own. A link is drawn when both endpoints belong to the selected set — including a dependency on a package whose children were selected, since pkg.* never selects pkg itself.

Errors

Bad input is reported on stderr and exits 2; an analysis that cannot finish exits 1 (diff has its own codes, below). Nothing fails silently — a mistyped metric name is an error listing the valid ones, and a pattern matching no modules is an error rather than an empty diagram.

$ arch-blueprint examples/project_root -m 'app1.*' --metric fanin
arch-blueprint: unknown metric 'fanin'. Available: depth, edge_weight, fan_in, fan_out, instability

Metrics

--metric NAME is repeatable and displays a metric. Where it is drawn depends on what it measures:

Metric Kind Drawn as
fan_in node a row in the node's block
fan_out node a row in the node's block
instability node a row in the node's block — fan_out / (fan_in + fan_out)
edge_weight link a label on the connection: how many imports it stands for

Blocks appear in the order you asked for them. A cycle is one connection standing for two links, so a link metric shows both values there as forward/backward, matching the order of the cycle's own detail block.

arch-blueprint tests/fixtures/cyclic -m 'pkg_a.*' -m 'pkg_b.*' \
  --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 connection is a cycle, so edge_weight reads 2/1: two imports one way, one the other — the same two directions the note spells out.

New metrics are self-contained plugins under src/arch_blueprint/metrics/, registered in metrics/__init__.py — no changes to the extractor or renderers are needed. See CLAUDE.md for the protocols.

Snapshots, render and diff

-f json writes a snapshot of the graph instead of a diagram: modules, the imports between them and every metric. Links, cycles and namespace containers are not stored — they are derived from the imports again on load, so a snapshot cannot hold a stale copy of them. Every diagram can be drawn from a snapshot, byte for byte the same as a direct run:

arch-blueprint src -m 'myapp.*' -f json > graph.json
arch-blueprint render graph.json -f puml --metric fan_in > graph.puml

diff draws what changed between two snapshots, in puml or d2:

arch-blueprint diff old.json new.json -f puml > diff.puml

It needs no stored files when the project is in git: --base REV builds the graph at that revision (git archive into a temporary directory — no worktree, nothing left in .git) and compares it with the working tree, or with --head REV. A package that exists on one side only is shown as added or removed rather than failing the run.

arch-blueprint diff --base origin/master src -m 'myapp.*' > diff.puml

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

The change is drawn over the whole graph: everything that did not change looks as on a plain diagram (depth colors, plain arrows, cycles as a red <->), and every change is dashed and in a color 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 (with --cycle-details, plus a note listing its imports)
cycle resolved — grey dashed arrow, cycle resolved, in the direction that remains (a bare line if neither does)

On a large project --changes-only draws just the changes and the unchanged modules their imports connect, without the unchanged dependencies.

diff and history are for a quick look at what changed, so the notes listing every import on a cycle are off there; --cycle-details turns them on. Every marker carries text as well as color, so a grey-scale image stays readable. Nothing changed still gives a valid diagram — the graph, with "No architectural changes" in the legend — so a CI job always has a picture to post.

diff exits like diff(1): 0 when nothing changed, 1 when something did, 2 on any error. The diagram is written either way; to keep a drawing step green on a diff but red on an error:

arch-blueprint diff --base origin/master src -m 'myapp.*' > diff.puml || test $? -eq 1

A module replaced by a package of the same name (api.py → api/) is drawn inside that package, since no diagram can have one name be both a module and a container. A structural diff ignores metrics and depth colors (depth shifts whenever the graph does), and treats a change to the imports inside a link present on both sides as no change. Graphing arch_blueprint itself always resolves to the running copy, so diff --base cannot compare two versions of this tool.

History album

history walks the branch's first-parent history (one commit per merged merge request) and, for every commit that changed the graph, draws the full diagram and the diff against the frame before it (over the whole graph, like diff; --changes-only for just the changes). Commits that leave the graph alone are skipped, so the album is the architecture's changes and nothing else:

arch-blueprint history src myapp                                   # everything: myapp.**
arch-blueprint history src app1 app2 -m 'app1.*' -m 'app2.core.*'  # roots, narrowed by -m
arch-blueprint history src myapp --base v1.0 --head master -f d2-png -o album

The roots are required, one or more top-level packages. Without -m each is graphed with everything under it (ROOT.**); with -m only those patterns are, and each must lie under one of the roots. A root that a commit does not have yet — or has only as a directory with no Python in it — is no error: the commit is reported as no source yet, and the root shows up in the frame where its code appears.

An album holds one kind of file, so it is easy to leaf through. -f picks it:

-f Files
puml (default), d2 diagram sources
puml-png, d2-png PNG images only, drawn with plantuml / d2 from PATH (checked before any work starts)
album/
  0001_2026-05-02_ab12cd3.png         the first frame: the diagram only
  0002_2026-05-12_ef45ab6.diff.png    what changed
  0002_2026-05-12_ef45ab6.png         what it became
  index.md                            the frames in order, with dates and commit subjects

Everything is cached in ./.arch-blueprint (or --cache-dir): every commit's snapshot, keyed by the project's git tree, and every image, keyed by the diagram it shows. If drawing fails, the run exits 1 and a rerun builds nothing and draws only the images still missing; another album of the same history reuses them too. d2 refuses to rasterize a very large diagram; such a diagram is redrawn at half the scale, then half again, and --scale FACTOR (d2-png only) sets the starting scale. PlantUML crops an image at 4096 px unless told otherwise; history raises that to 16384 (PLANTUML_LIMIT_SIZE, your own value wins). A file whose content is unchanged is not rewritten, and frame files of the same kind from an earlier run that this one did not produce are removed; nothing else in the directory is touched. A commit whose code cannot be analyzed is reported as skipped with the reason.

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)

Examples

Generated with the code in this repository against released packages, so they can be reproduced:

pip install --target /tmp/pkgs wemake-python-styleguide fastapi taskiq
arch-blueprint /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

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 connection is red. Compare it with the two below, where the red links are cycles the tool found.

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 /tmp/pkgs -m 'taskiq.*' \
  --metric fan_in --metric fan_out --metric instability

Every node carries its own block. 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. Cycles stay highlighted, with the imports that cause them listed beside the connection.

Source: docs/images/taskiq_metrics.puml

Taskiq module graph with metrics

License

MIT — see LICENSE.

Release files for arch-blueprint 0.2.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.2.0
File Size Uploaded
arch_blueprint-0.2.0.tar.gz 1.7 MB Details

Built distribution (wheel)

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

Total release size: 1.8 MB

Release files / arch_blueprint-0.2.0.tar.gz

Download URL arch_blueprint-0.2.0.tar.gz
Size 1.7 MB
Tags Source
SHA-256 checksum
How to use checksums
53b98f521f4943c0b3ab2a27966a7a4da8b0cce614384ad9f4a8c3299fde31b6
BLAKE2b-256 checksum
How to use checksums
8baf630808e17e97e87d502fdf4eeb4ac15ef60f6b246c52acf5c5ce20d87d7b
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.12.19 {"installer":{"name":"uv","version":"0.12.19","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 / arch_blueprint-0.2.0-py3-none-any.whl

Download URL arch_blueprint-0.2.0-py3-none-any.whl
Size 65.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
7c893ceeec48e41a42546c6c7217a5bfe4e94a490e1d2fa53571ce47b2f95ca1
BLAKE2b-256 checksum
How to use checksums
3b4cf4c3da0938b71f3860fa6dcbabaf15a1f741bfec5238e39371012cbdb5a7
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.12.19 {"installer":{"name":"uv","version":"0.12.19","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 history Release notifications | RSS feed

This release

0.2.0 This release

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