Skip to main content

wasi-graphviz

Run Graphviz from Python via WebAssembly — no native Graphviz binary, no Node.js, no browser required.

Why this exists

Rendering DOT graphs from Python sits in an awkward gap.

  • Browser-based WASM renderers like @hpcc-js/wasm-graphviz ship Graphviz as a WebAssembly module — but it's an Emscripten build wrapped in JavaScript glue, designed to run in a browser or Node. Tools like easydot wrap that flow so you can call it from Python and display SVGs in a notebook, but the renderer itself still ultimately needs a JS runtime / browser surface to produce pixels — fine for interactive notebooks, awkward for headless static SVG generation in CI, batch jobs, or server-side pipelines.
  • System-binary Python wrappers like graphviz and pygraphviz shell out to a system-installed dot or link against libcgraph. That means every deployment target — laptops, CI runners, Lambda functions, Docker base images — has to install Graphviz separately, and your code has to spawn subprocesses or manage shared-library loading.

wasi-graphviz plugs the gap: a wasm32-wasi build of upstream Graphviz plus a thin Python wrapper. pip install is the entire install story, the wheel is ~440 KB, and rendering happens entirely in-process — no subprocesses, no system packages, no JS, no browser. Works the same in a notebook, a CI job, a serverless function, or an offline air-gapped environment.

The wheel itself does not include a WASM runtime — that's an explicit choice (see Installation below) so you can pick the runtime that matches your deployment constraints.

Installation

wasi-graphviz needs a runtime to execute the bundled graphviz.wasm. Two are supported, picked via extras:

# Recommended for most users — fast native runtime
pip install wasi-graphviz[wasmtime]

# Pure-Python runtime — slow, but works anywhere CPython does (not Pyodide)
pip install wasi-graphviz[pywasm]

# Install both — `render(..., backend="auto")` will prefer wasmtime
pip install wasi-graphviz[all]

Choosing a backend

wasmtime pywasm
Implementation Native runtime (Rust) with Python bindings Pure-Python WASM interpreter
Speed ~0.3–7 ms per render (see below) ~3–130 s per render — 4–5 orders of magnitude slower
Install size ~15 MB wheel (compiled extensions) ~200 KB wheel
Platforms Linux/macOS/Windows on x86_64 + arm64 Anywhere CPython 3.11+ runs (see Pyodide note below)
Cold start Slightly heavier instantiation Fastest to import
Use when… Production, CI, notebooks, anything performance-sensitive Last-resort portability — exotic CPU/OS, no-native-deps environments

backend="auto" (the default) prefers wasmtime when available and silently falls back to pywasm, so most code can ignore the distinction. Force one explicitly when you have a reason — see Backend selection below.

Pyodide / marimo / WebAssembly-based Python environments

pywasm is pure-Python, but it imports fcntl (for stdin handling) which is not available in Pyodide because Pyodide itself runs inside a browser WebAssembly sandbox that lacks POSIX file-control APIs.

Neither backend works in Pyodide today. A future browser / pyodide backend (using the browser's native WebAssembly object + a WASI polyfill) is possible, but not yet implemented.

If you need Graphviz in a Pyodide or marimo notebook, use easydot instead — it wraps the browser's @hpcc-js/wasm renderer and works out of the box in those environments.

Benchmarks

Median wall time per render on Apple M-series, Python 3.11, measured via pytest-benchmark (run yourself with uv run pytest tests/test_benchmarks.py -m perf --benchmark-only):

Graph wasmtime pywasm wasmtime speedup
10 edges 0.29 ms 3.7 s ~12,800 ×
100 edges 1.74 ms 32.5 s ~18,700 ×
400 edges 6.86 ms 133.3 s ~19,400 ×

pywasm is a pure-Python WASM interpreter, so the ratio is roughly "interpreted Python evaluating WASM bytecode" vs "native compiled code" — expect orders of magnitude, not factors. Use wasmtime unless your environment forbids native code.

Quick start

from wasi_graphviz import render

# Render a simple graph to SVG (uses wasmtime if available, falls back to pywasm)
svg = render("digraph G { a -> b; }")
print(svg.decode("utf-8"))

Usage

Basic rendering

from wasi_graphviz import render

# Render to SVG with default dot engine
svg = render("digraph G { a -> b; }")

# Use a different layout engine
svg = render("graph G { a -- b; }", engine="neato")

# Render to DOT format
output = render("digraph G { a -> b; }", format="dot")

Rendering graphs with image assets

Pass image files as bytes with portable relative names. Graphviz sees these files under the guest path /assets; it cannot read arbitrary files from the host. Use ASCII letters, digits, ., _, -, and / in asset names.

from pathlib import Path
from wasi_graphviz import render

glyph = Path("glyph.svg").read_bytes()
dot = '''digraph G {
    node [shape=none, image="/assets/glyph.svg", label=""]
    glyph
}'''
svg = render(dot, assets={"glyph.svg": glyph}, backend="pywasm")

With this WASM build, SVG image files should start with an XML declaration (<?xml version="1.0" encoding="UTF-8"?>) so Graphviz recognizes their dimensions.

assets works with auto, wasmtime, and pywasm. For SVG output, referenced .svg, .png, .gif, .jpg, .jpeg, and .jpe files are embedded as data URIs, so the returned graph remains usable after the temporary files are removed. Only referenced image files need one of these extensions; unused files such as metadata can use other names. A referenced image with no supported suffix raises ValueError because Graphviz and the SVG output need its image type. Embedding applies to Graphviz format selectors whose base is svg or svg_inline, including svg:svg:core and svg_inline:svg:core. If imagepath points to /assets or one of its subdirectories, relative image paths are embedded when they uniquely match a supplied asset. If a relative image path matches multiple supplied files, use an explicit path such as image="/assets/icons/glyph.svg" to identify it.

Backend selection

See the trade-off table above for when to pick which.

from wasi_graphviz import render

# Auto-select (prefer wasmtime, fall back to pywasm)
svg = render("digraph G { a -> b; }", backend="auto")

# Force pywasm — pure Python, slow but maximally portable
svg = render("digraph G { a -> b; }", backend="pywasm")

# Force wasmtime — fast native runtime, requires compiled extension
svg = render("digraph G { a -> b; }", backend="wasmtime")

Error handling

from wasi_graphviz import render, RenderError

try:
    svg = render("not valid dot {")
except RenderError as e:
    print(f"Render failed: {e}")

Supported layout engines

All major Graphviz layout engines work:

  • dot — hierarchical layouts (default)
  • neato — spring model
  • circo — circular layout
  • fdp — force-directed placement
  • sfdp — scalable FDP
  • twopi — radial layouts
  • osage — array-based layouts
  • patchwork — treemaps

Supported output formats

The core plugin supports:

  • svg (default)
  • dot
  • json
  • ps
  • map
  • fig
  • tk

Architecture

The project consists of three layers:

  1. WASM artifact (graphviz.wasm)

    • Graphviz 16.1.0 compiled for wasm32-wasi
    • Exposes a plain C ABI: graphviz_render, graphviz_free, graphviz_last_error, graphviz_version
    • No Emscripten, no JS glue
  2. Python backends

    • PywasmBackend — pure-Python interpreter with built-in WASI support
    • WasmtimeBackend — fast native runtime with full WASI support
  3. Public API

    • render(dot, format="svg", engine="dot", backend="auto", assets=None) -> bytes

Building from source

See BUILD.md for detailed build instructions.

Quick summary:

# Install build tools
pixi install

# Build and validate the WASM artifact
pixi run build-wasm

Development

# Run tests (perf benchmarks are skipped by default)
uv run pytest

# Format and lint
uv run ruff check .
uv run ruff format .

# Run benchmarks comparing wasmtime vs pywasm across graph sizes
uv run pytest tests/test_benchmarks.py -m perf --benchmark-only

Automated releases

The scheduled Graphviz update workflow creates a patch release only after the new WASM build, linting, tests, and package build succeed. It authenticates its commit and tag push with a short-lived GitHub App installation token.

To enable it for a repository, install a GitHub App with Contents: Read and write access and configure:

  • repository variable RELEASE_APP_CLIENT_ID with the App's client ID;
  • repository secret RELEASE_APP_PRIVATE_KEY with the App's private key.

The App must be installed on this repository. No personal access token is required.

License & attribution

This package is licensed under the Eclipse Public License 2.0 (EPL-2.0). See LICENSE for the full text.

The wheel bundles a compiled build of Graphviz (also EPL-2.0). The full EPL-2.0 text is also shipped inside the wheel at wasi_graphviz/assets/GRAPHVIZ_LICENSE. Source for the bundled Graphviz version is available upstream: https://gitlab.com/graphviz/graphviz/-/tree/16.1.0.

Modifications applied to the Graphviz source before compilation are described in scripts/prepare_graphviz_wasi.py and are themselves licensed under EPL-2.0. See NOTICE for the full attribution.

wasi-graphviz is an unofficial repackaging and is not affiliated with or endorsed by the Graphviz project.


First functional v0.1.0 built with Kimi K2.6 in ~1h, single session (81% context used). Total cost: ~$1

Metadata

Release files for wasi-graphviz 0.1.4

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

Source distribution (sdist)

Source distribution for wasi-graphviz 0.1.4
File Size Uploaded
wasi_graphviz-0.1.4.tar.gz 408.3 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for wasi-graphviz 0.1.4
File Interpreter ABI Platform
wasi_graphviz-0.1.4-py3-none-any.whl Python 3 none any Details

Total release size: 822.7 kB

Release files / wasi_graphviz-0.1.4.tar.gz

Download URL wasi_graphviz-0.1.4.tar.gz
Size 408.3 kB
Tags Source
SHA-256 checksum
How to use checksums
302f3f0e17ba89e5674ee7e9f8ca6d4c3f981390a8b43eb5567693bbc9fccd02
BLAKE2b-256 checksum
How to use checksums
c5990896dce64dbf1dffd29348a87705025312e09e31bc982e8d3c7fcc906c78
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 22, 2026.

Transparency log

Release files / wasi_graphviz-0.1.4-py3-none-any.whl

Download URL wasi_graphviz-0.1.4-py3-none-any.whl
Size 414.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
c3090df62ea035fb5ba8aceb6b1e66a46ff2db301fabac9a3090dd652e683862
BLAKE2b-256 checksum
How to use checksums
f1cb6f97bc7e3fea7bee778d07fc8564ab6f860c1d044d1c2cad83ffcad63def
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 22, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.1.4 This release

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