This guide is for maintainers, contributors, and architects who need to understand how Diagrams works internally: the global-context rendering model, the operator-overloading DSL, the Graphviz integration, and — most importantly — the icon-to-class auto-generation pipeline that produces the library's ~2,400 provider node classes. It also covers how to add a provider or node, the test strategy, security considerations for the CLI, and known limitations.
For end-user installation and diagram syntax, see the companion User Guide.
1. Executive Summary
Diagrams is a pure-Python library that turns Python code into cloud-architecture images. It is a thin, elegant layer over Graphviz: the library's job is to (1) provide an ergonomic Python DSL for declaring nodes and edges, and (2) attach the correct icon image to each node. All actual graph layout and rendering is delegated to Graphviz via the graphviz Python binding.
The codebase has two clearly separated halves:
| Half | What it is | How it's maintained |
|---|---|---|
Runtime library (diagrams/__init__.py, diagrams/c4, diagrams/custom) |
The hand-written core: Diagram, Cluster, Node, Edge and the DSL. ~620 lines. |
Edited by hand. |
Provider node classes (diagrams/<provider>/*.py) |
~2,400 three-line classes mapping a class name to an icon PNG. | Auto-generated by autogen.sh from resources/*.png. Never hand-edited. |
This separation is the single most important architectural fact about the project. The runtime is small and stable; the provider surface is large and mechanically generated. Understanding the generator (scripts/, config.py, templates/) is essential before touching anything under diagrams/<provider>/.
| Attribute | Value |
|---|---|
| Language | Python (requires-python ~=3.9) |
| Version (at this commit) | 0.25.1 |
| License | MIT |
| Runtime dependencies | graphviz, jinja2 |
| System dependency | Graphviz (the dot binary) |
| Build backend | Hatchling |
| Console script | diagrams = diagrams.cli:main |
| Homepage | https://diagrams.mingrammer.com |
2. Architecture Overview
Author writes diagram.py
│
▼
┌──────────────────────────────────────────────┐
│ diagrams/__init__.py (the runtime core) │
│ │
│ Diagram ── context mgr, holds graphviz.Digraph
│ Cluster ── context mgr, builds subgraph │
│ Node ── base; >> << - operator overloads │
│ Edge ── connection attrs (label/color) │
│ │
│ contextvars: __diagram, __cluster (globals) │
└───────────────────┬──────────────────────────┘
│ provider node classes subclass Node
▼
┌──────────────────────────────────────────────┐
│ diagrams/<provider>/<category>.py │
│ class EC2(_Compute): _icon = "ec2.png" │ ← AUTO-GENERATED
│ _icon_dir = "resources/aws/compute" │
└───────────────────┬──────────────────────────┘
│ Node._load_icon() resolves PNG path
▼
┌──────────────────────────────────────────────┐
│ graphviz.Digraph → dot (native Graphviz) │
│ .render() → web_service.png / .svg / .pdf │
└──────────────────────────────────────────────┘
Build-time (maintainers only):
resources/<provider>/*.png ──► scripts/generate.py ──► diagrams/<provider>/*.py
(config.py rules + templates/module.tmpl, via autogen.sh)
The two execution contexts
- Author runtime — when a user runs
python diagram.py. Onlydiagrams/__init__.pyand the provider class files are involved. Graphviz renders the output. - Maintainer build — when icons change.
autogen.shpreprocesses SVG/PNG resources and regenerates every provider module. End users never run this.
3. The Runtime Core (diagrams/__init__.py)
The entire user-facing runtime is ~620 lines in a single module. Four classes and a global context.
3.1 Global context via contextvars
Diagrams avoids threading a diagram object through every call by storing the "current diagram" and "current cluster" in module-level contextvars.ContextVar objects:
__diagram = contextvars.ContextVar("diagrams")
__cluster = contextvars.ContextVar("cluster")
def getdiagram(): ... # returns current Diagram or None
def setdiagram(d): ...
def getcluster(): ... # returns current Cluster or None
def setcluster(c): ...
Diagram.__enter__callssetdiagram(self);__exit__callsrender(), removes the intermediate.dotfile, thensetdiagram(None).Cluster.__enter__callssetcluster(self);__exit__attaches its subgraph to its parent (or the diagram) and restores the previous cluster.Node.__init__readsgetdiagram()andgetcluster()to know where to attach itself.
Note
Using
contextvars(rather than plain globals) means the context is coroutine/thread-local-aware. However, the library is not designed for concurrent diagram construction in the same context — build one diagram at a time within awithblock.
Important
If
getdiagram()returnsNonewhen aNodeorClusteris constructed, the constructor raisesEnvironmentError("Global diagrams context not set up"). This is the enforcement mechanism that requires all nodes to live inside awith Diagram(...)block.
3.2 Diagram
Diagram wraps a graphviz.Digraph. Construction:
- Validates
themeagainstTHEMES(raisesValueErrorif unknown) and stores the theme's color config. - Derives
filenamefromnameif not given ("Web Service"→web_service); falls back todiagrams_imageif both are empty. - Creates
graphviz.Digraph(name, filename=filename, strict=strict). - Applies
_default_graph_attrs,_default_node_attrs,_default_edge_attrs(the visual defaults), then the theme edge color. - Validates and applies
direction→rankdir,curvestyle→splines. - Validates
outformat(string or list) against the allowed set. - Merges user-supplied
graph_attr/node_attr/edge_attrover the defaults.
Validation tables (class attributes):
| Attribute | Allowed values |
|---|---|
__directions |
TB, BT, LR, RL |
__curvestyles |
ortho, curved, spline, polyline |
__outformats |
png, jpg, svg, pdf, dot |
Default visual attributes (the source of the library's signature look):
_default_graph_attrs = {"pad": "2.0", "splines": "spline", "nodesep": "0.70",
"ranksep": "0.90", "fontname": "Sans-Serif",
"fontsize": "15", "fontcolor": "#2D3436"}
_default_node_attrs = {"shape": "box", "style": "rounded", "fixedsize": "true",
"width": "1.4", "height": "1.4", "labelloc": "b",
"imagepos": "tc", "imagescale": "true", "fontname": "Sans-Serif",
"fontsize": "13", "fontcolor": "#2D3436"}
_default_edge_attrs = {"color": "#495057", "arrowsize": "0.8"}
render() calls self.dot.render(format=..., view=self.show, quiet=True) once per requested format (it loops if outformat is a list).
3.3 Themes
Themes are a recent feature. THEMES is a dict of five palettes; each defines a tuple of cluster background colors (indexed by nesting depth), a pencolor (cluster border), and an edgecolor:
| Theme | Use |
|---|---|
neutral (default) |
Grays — the classic look. |
pastel |
Soft multi-hue. |
blues / greens / orange |
Monochrome ramps. |
Cluster reads the active diagram's _theme_config to pick its background color: coloridx = self.depth % len(bgcolors), so deeply nested clusters cycle through the palette.
Note
Because
theme/autolabelpost-date the 0.25.1 PyPI release, the version string inpyproject.tomllags the source feature set (tracked upstream as a version-mismatch issue). Documentation generated from source — like this guide — reflects the code at commit23f06b4, which includes themes.
3.4 Node — the base of every provider class
Node is the abstract base. Every provider class is ultimately class X(Node). Key class attributes a subclass overrides: _provider, _type, _icon_dir, _icon, _height.
Construction logic:
- Generates a random
uuid4().hexnode id unlessnodeid=is passed. - Requires an active diagram (else
EnvironmentError). - If the diagram has
autolabel=True, prepends the class name to the label. - If the class has an
_icon, builds Graphviz attrs{"shape": "none", "height": ..., "image": self._load_icon()}; otherwise an empty attrs dict (a labelled box). _load_icon()resolves the icon path relative to the package directory:os.path.join(<package parent>, self._icon_dir, self._icon).- Attaches itself to the current cluster if one is active, else to the diagram.
Tip
The
Customnode overrides_load_icon()to return a caller-supplied path verbatim, which is how user images bypass theresources/lookup.
3.5 The connection DSL — operator overloading
The >>, <<, and - syntax is implemented by overloading Python operators on Node and Edge:
| Python dunder | Operator | Effect |
|---|---|---|
__rshift__ / __rrshift__ |
>> |
Forward edge (Edge(self, forward=True)). |
__lshift__ / __rlshift__ |
<< |
Reverse edge (Edge(self, reverse=True)). |
__sub__ / __rsub__ |
- |
Undirected edge. |
The "r"-prefixed reflected dunders (__rrshift__, etc.) exist specifically so that a Python list on the left of an operator works — e.g. [a, b] >> c. Lists don't define these operators, so Python falls back to the right operand's reflected method, which iterates the list and connects each element. This is why you can fan-in/fan-out with list literals but cannot put a list on both sides.
Each operator returns the right-hand operand so chains (a >> b >> c) compose left-to-right. The actual edge is added on the diagram (not the cluster) via Diagram.connect() → self.dot.edge(...), because Graphviz requires cross-cluster edges to be declared at the top level.
3.6 Edge
Edge holds connection attributes. The constructor accepts node, forward, reverse, label, color, style, and arbitrary **attrs (passed straight to Graphviz). The computed attrs property maps the forward/reverse booleans to Graphviz's dir attribute (forward/back/both/none). Edge overloads the same operators as Node to support edge chaining and labeling mid-flow.
Note
A historical workaround lives here: edge labels are stored under the Graphviz
labelkey (an earlierxlabelapproach caused misaligned labels — see upstream issue #83).
3.7 Cluster / Group
Cluster creates a Graphviz subgraph whose name is prefixed cluster_ (Graphviz treats cluster*-named subgraphs specially, drawing a bounding box). Depth is computed from the parent cluster; the background color is chosen from the theme palette by depth. Group = Cluster is a simple alias.
Warning
A
FIXMEin the source notes that per-clusterdirectiondoes not currently work — Graphviz cannot reliably render a subgraph with a different rank direction than its parent. Don't rely onCluster(direction=...).
4. The CLI (diagrams/cli.py)
The console entry point is intentionally minimal:
def run() -> int:
parser = argparse.ArgumentParser(description="Run diagrams code files in a diagrams environment.")
parser.add_argument("paths", metavar="path", type=str, nargs="+",
help="a Python file containing diagrams code")
args = parser.parse_args()
for path in args.paths:
with open(path, encoding='utf-8') as f:
exec(f.read())
return 0
It takes one or more file paths and exec()s each file's contents. There are no other flags.
Caution
The CLI executes arbitrary Python via
exec(). Never rundiagramson a.pyfile you do not trust — it has the same security profile aspython <file>. This is a deliberate convenience for batch-rendering trusted diagram scripts in CI, not a sandbox. See Security.
5. The Auto-Generation Pipeline (Maintainer-Critical)
This is the part of the codebase that surprises newcomers: the provider node classes are generated, not written. Editing diagrams/aws/compute.py by hand is wrong — it will be overwritten. The pipeline lives in autogen.sh, config.py, scripts/, and templates/.
5.1 Inputs and outputs
| Input | Output |
|---|---|
resources/<provider>/<category>/<icon>.png (and source SVGs) |
diagrams/<provider>/<category>.py (node classes) |
config.py (naming rules, aliases, prefixes) |
docs/nodes/<provider>.md (node index docs) |
templates/*.tmpl (Jinja2) |
website/static/img/resources/... (icons copied for the site) |
5.2 autogen.sh — the orchestrator
The script (run from the repo root) does, for each provider in its providers array:
- Preprocess resources depending on provider:
onprem/azure: convert SVG → PNG with Inkscape (python -m scripts.resource svg2png).oci/ibm: convert SVG → PNG with ImageMagick (svg2png2).- All: clean resource file names (
scripts.resource clean). aws: round the PNG corners (scripts.resource round).
- Generate modules & docs:
python -m scripts.generate <provider>for every provider, pluscustom. - Copy icons into the website static folder.
- Lint the generated modules with
black.
Important
autogen.shrequires external tools on the maintainer's machine:round,inkscape,convert(ImageMagick), andblack. It exits early if any are missing. These are build-time only — end users need none of them.
5.3 config.py — the naming brain
config.py is the single source of truth for how an icon filename becomes a Python class name. Key tables:
| Table | Purpose |
|---|---|
PROVIDERS |
The canonical provider list (drives generation order). |
FILE_PREFIXES |
Vendor prefixes stripped from filenames (e.g. AWS Amazon-, AWS-; Azure Azure-; OCI OCI-icon-). |
UPPER_WORDS |
Tokens force-uppercased per provider (e.g. ec2, vpc, rds, gke, kms) so class names read correctly. |
TITLE_WORDS |
Special title-casing (e.g. onprem → OnPrem, digitalocean → DigitalOcean). |
ALIASES |
Short alternate class names per provider/category (e.g. SimpleStorageServiceS3 → S3, ElasticKubernetesService → EKS). |
It also defines the resource-processing commands (CMD_ROUND, CMD_SVG2PNG = Inkscape with 256×256 export, CMD_SVG2PNG_IM = ImageMagick) and the directory layout (DIR_DOC_ROOT="docs/nodes", DIR_APP_ROOT="diagrams", DIR_RESOURCE="resources", DIR_TEMPLATE="templates").
5.4 Generated module shape
Every generated category module follows the same pattern (here, AWS compute):
# This module is automatically generated by autogen.sh. DO NOT EDIT.
from . import _AWS
class _Compute(_AWS):
_type = "compute"
_icon_dir = "resources/aws/compute"
class EC2(_Compute):
_icon = "ec2.png"
# ... one 3-line class per icon, plus alias assignments at the bottom
The provider's __init__.py defines the private _<Provider> base (setting _provider and the top-level _icon_dir); each category module defines a private _<Category> base that points _icon_dir at the category's resource folder; each node is a leaf class whose only body is _icon = "<file>.png". Aliases from config.py are appended as simple assignments (ECR = EC2ContainerRegistry).
6. Repository Layout
diagrams/
├── diagrams/ # the importable package
│ ├── __init__.py # ★ runtime core (Diagram/Cluster/Node/Edge)
│ ├── cli.py # console entry point (exec diagram files)
│ ├── base/ # internal base nodes
│ ├── custom/ # Custom (user-supplied icon) node
│ ├── c4/ # C4-model helpers (hand-written)
│ ├── aws/ azure/ gcp/ k8s/ onprem/ ... # AUTO-GENERATED provider modules
│ └── <17 more providers>
├── resources/ # source icon PNGs/SVGs (input to generation)
├── scripts/ # generate.py, resource.py (the generators)
├── templates/ # Jinja2 templates (module.tmpl, apidoc.tmpl)
├── docs/ # docusaurus site source (getting-started, guides, nodes)
├── website/ # docusaurus site (v1) + copied icons
├── tests/ # pytest suite (test_diagram, test_c4, test_cli)
├── autogen.sh # ★ regenerate everything from resources/
├── config.py # ★ naming rules / aliases / prefixes
└── pyproject.toml # hatchling build, deps, console script
The three ★ files plus the runtime __init__.py are where all the real logic lives; everything under the provider directories is derived.
7. Dependencies
| Dependency | Constraint | Role |
|---|---|---|
graphviz (PyPI) |
>=0.13.2,<0.22.0 |
Python binding that shells out to the dot binary. The whole render path runs through graphviz.Digraph. |
jinja2 |
>=2.10,<4.0 |
Used by the generation scripts to render module/doc templates. Not used at author runtime. |
typed-ast |
>=1.5.5,<2, only python_version<'3.8' |
Legacy back-compat shim; inert on supported (3.9+) Pythons. |
System requirement: Graphviz must be installed separately (the graphviz PyPI package is only a binding, not the engine).
Note
Because the only hard runtime dependency that does work is the
graphvizbinding, Diagrams has a very small attack/maintenance surface.jinja2matters only to maintainers runningautogen.sh.
8. Security Considerations
| Concern | Assessment |
|---|---|
| Arbitrary code execution (CLI) | diagrams/cli.py runs exec(open(path).read()) on every file argument. Treat input files as code. Only run the CLI on trusted diagram scripts. |
Custom node + remote icons |
Examples download icons via urllib.request.urlretrieve(url). Fetching an attacker-controlled URL writes an arbitrary file to the working directory. Pin/validate icon sources in shared environments. |
| No network/cloud access | The library performs no cloud API calls and holds no credentials — it only reads local icon files and writes image output. There is no secret-handling surface. |
| Graphviz as the trust boundary | All untrusted content ultimately flows into Graphviz's dot. Keep Graphviz patched; it (not Diagrams) is the component parsing/rendering. |
| Output side effects | Diagram.__exit__ calls os.remove(self.filename) to delete the intermediate DOT file, and show=True launches the OS image viewer. In CI, always set show=False. |
Caution
Do not expose the
diagramsCLI as a service that renders user-submitted files.exec()of untrusted input is remote code execution by design. If you must accept user diagram code, run it in a locked-down sandbox (container, seccomp, no network, read-only FS) — the library provides none of that itself.
9. Operations & CI
Diagrams has no runtime to "operate" — it's a library. The operational concerns are about rendering in automation:
- [ ] Install Graphviz on the runner (
apt-get install graphviz/brew install graphviz/choco install graphviz). - [ ]
pip install diagramsinto the same environment the scripts run in (avoidpipxisolation). - [ ] Run each diagram with
show=False(no viewer in headless CI). - [ ] Render via
python diagram.pyor batch withdiagrams *.py. - [ ] Commit the
.pysources; treat generated images as build artifacts (regenerate, don't hand-edit).
Regenerating provider modules (maintainers)
# requires: inkscape, imagemagick (convert), round, black on PATH
$ ./autogen.sh
starting
converting the svg to png using inkscape for provider 'onprem'
cleaning the resource names for provider 'aws'
rounding the resources for provider 'aws'
generating the modules & docs for provider 'aws'
...
linting the all the diagram modules
Tip
After
autogen.sh, review thegit diffunderdiagrams/<provider>/to confirm only intended icons changed. A noisy diff usually means a resource rename or a new alias is needed inconfig.py.
10. Testing Strategy
The tests/ suite (pytest) covers the hand-written runtime, not the generated classes:
| Test file | Covers |
|---|---|
tests/test_diagram.py |
Diagram/Cluster/Node/Edge construction, operator behavior, options validation, output. |
tests/test_c4.py |
The C4 helper functions and label formatting. |
tests/test_cli.py |
The CLI's file-execution behavior. |
Dev tooling (from pyproject.toml): pytest, pylint, isort, black (line length 120), pre-commit, rope. The repo ships a .pre-commit-config.yaml running check-yaml, end-of-file-fixer, trailing-whitespace, seed-isort-config, isort, and autopep8 (aggressive, max-line-length 120).
Note
The generated provider classes are not individually unit-tested — they are mechanically identical, so testing the generator and the
Nodebase is sufficient. If you change the template orconfig.py, regenerate and run the full suite plus a visual smoke test (render one diagram per provider).
11. Extending the Library
Add a node to an existing provider
- Drop the icon PNG (or SVG, for providers that convert) into
resources/<provider>/<category>/. - If the filename needs cleanup, add prefix/alias/casing rules to
config.py(FILE_PREFIXES,UPPER_WORDS,TITLE_WORDS,ALIASES). - Run
./autogen.sh(orpython -m scripts.generate <provider>). - Verify the new class appears in
diagrams/<provider>/<category>.pyand renders.
Warning
Never add the class by editing the generated file. Add the icon and (if needed) a config rule, then regenerate. Hand edits are lost on the next generation.
Add a whole provider
- Add the provider name to
PROVIDERSinconfig.pyand theprovidersarray inautogen.sh. - Populate
resources/<provider>/<category>/...with icons. - Add any prefix/alias/casing entries for the provider in
config.py. - Run
./autogen.sh. The provider's__init__.py, category modules, and docs are generated from the templates.
Add a hand-written abstraction (like C4)
C4 (diagrams/c4/__init__.py) is the model for hand-written, non-generated modules: it composes the core Node, Cluster, and Edge primitives into higher-level helpers (Person, System, Container, Database, SystemBoundary, Relationship) using HTML-style Graphviz labels. Follow this pattern for any new conceptual layer that isn't a flat icon catalog.
12. Known Issues, Limitations & Improvement Ideas
Drawn from the source (FIXME/TODO markers) and community-reported issues:
| Area | Issue | Notes / suggested direction |
|---|---|---|
| Cluster direction | Cluster(direction=...) is a no-op (FIXME in source). |
Graphviz subgraph rank-direction limitation; document as unsupported or detect and warn. |
| Dense-graph edges | ortho/spline routing produces broken/floating edges on large diagrams (community #1214; upstream Graphviz). |
Offer guidance to switch curvestyle, use clusters, or split diagrams; consider smarter default ports. |
| Edge ports | Edges can float off icon-shaped nodes; request for direction-based default tailport/headport (#1215). |
Compute default ports from direction to keep arrows anchored. |
| Install via pipx | ModuleNotFoundError: diagrams.cli under pipx (#1149). |
Document non-pipx install (done in User Guide); consider a clearer import-error hint. |
| Icon regressions | Logos failed to render in 0.25.0 (#1186). | Add a generation-time check that every class's _icon file exists. |
| Version mismatch | pyproject.toml version lags shipped source features like themes (#1183). |
Align version bump with feature merges / automate from tags. |
| Dependency EOL | typed-ast is end-of-life (#832). |
Already gated to python<'3.8'; can be dropped once 3.7 support is gone. |
| Label position | Edge labels use label (not xlabel) due to misalignment (#83). |
Revisit if upstream Graphviz fixes xlabel positioning. |
| Imagepos compat | imagepos="tc" requires Graphviz ≥ 2.40 (TODO in source). |
Add a version check or graceful fallback for old Graphviz. |
| Node-class validation | config.py ALIASES has a "check if the classname exists" TODO. |
Validate alias targets during generation to catch typos. |
Build-quality wins
- The generator runs
blackon output but doesn't validate that every referenced icon exists — adding that check would have caught #1186. - Provider modules are fully mechanical; a generation-time schema (provider → category → icon manifest) committed alongside the code would make diffs reviewable without re-running the toolchain.
13. Quick Reference
| Item | Value |
|---|---|
| Install (lib) | pip install diagrams (Python ≥ 3.9) |
| Install (engine) | Graphviz (brew/choco/apt install graphviz) |
| Render a file | python diagram.py or diagrams diagram.py [more.py …] |
| Core imports | from diagrams import Diagram, Cluster, Group, Node, Edge |
| Node import path | from diagrams.<provider>.<category> import <Class> |
| Custom icon | from diagrams.custom import Custom |
| C4 model | from diagrams.c4 import Person, System, Container, Database, SystemBoundary, Relationship |
| Output formats | png (default), jpg, svg, pdf, dot |
| Directions | TB, BT, LR (default), RL |
| Curve styles | ortho, curved, spline (default), polyline |
| Themes | neutral (default), pastel, blues, greens, orange |
| Operators | >> forward · << reverse · - undirected |
| Regenerate providers | ./autogen.sh (maintainers; needs inkscape/imagemagick/round/black) |
| Naming rules | config.py (PROVIDERS, FILE_PREFIXES, UPPER_WORDS, TITLE_WORDS, ALIASES) |
| Tests | pytest (tests/test_diagram.py, test_c4.py, test_cli.py) |