mrkeyoor.com_
Thu 06 Aug 10:55 UTC
PyPIDataupdated 06 Aug 2026

graphviz

graphviz is a small Python package that builds DOT language source code and shells out to the Graphviz command line tools to draw it. You create a Digraph or Graph object, call .node() and .edge() to describe the picture, and the object keeps a plain text DOT string you can print, save, or render to PDF, PNG, or SVG. Rendering runs the dot binary as a subprocess, so the drawing quality and the layout algorithms are Graphviz's, not Python's. It also knows how to show itself inside Jupyter, which is why so many tree and pipeline visualisations in notebooks route through it.

Verdict

The shortest path from Python objects to a decent-looking static diagram, as long as you can install the Graphviz binaries alongside it. Treat it as a typed front end for the DOT language rather than a graph library, and reach for networkx or pydot the moment you need to analyse or re-read a graph.

API stability5/5Despite the 0.x version number, Digraph, node, edge, render, and pipe have worked the same way for years; recent releases added things like the outfile argument and Jupyter format switching without breaking existing scripts.
Docs4/5graphviz.readthedocs.io has a proper user guide, full API reference, a changelog, and runnable notebook examples. The gap is that most real questions are about DOT attributes and layout tuning, which live in the upstream Graphviz documentation this package does not restate.
Maintenance3/5One maintainer, version 0.21 released in June 2025 and the repository last pushed July 2026, with only about 10 open issues. The code is small and settled, so low activity is not alarming, but there is no bus factor and no roadmap.
Ecosystem5/5Around 13.5M downloads a week and the default rendering step behind scikit-learn's export_graphviz, notebook tutorials, and many internal diagram scripts, so answers to common problems are easy to find.

Use it if

  • You need a static diagram of something structural (a state machine, a dependency graph, a decision tree, an ER sketch) and you want the layout computed for you instead of placing boxes by hand
  • You are rendering DOT strings that some other tool already produced, for example sklearn.tree.export_graphviz output, a Django model graph, or a build system dependency dump
  • You work in Jupyter and want the graph to appear inline: the objects implement the notebook display protocol, so returning a Digraph from a cell draws it
  • You want the DOT source itself as an artifact you can diff, commit, and hand to any other Graphviz tool later
Skip it if

Setup reality

Two installs, not one. pip install graphviz gives you the Python layer; you also need the Graphviz distribution itself (apt install graphviz, brew install graphviz, or the Windows installer) with the dot executable on PATH, and the Windows installer historically did not add it for you. In Docker that means an extra apt-get line and roughly 100MB of system packages you may not have budgeted for. On conda the package name differs: python-graphviz pulls the graphviz binaries as a dependency, plain graphviz on PyPI does not. Rendering spawns a subprocess per call, so tight loops over hundreds of small graphs are slow for reasons no amount of Python tuning will fix, and errors from dot arrive as CalledProcessError with the tool's stderr attached rather than as a Python-level explanation.

Patterns

Build a directed graph and inspect the DOT sourcebuild-a-directed-graph

import graphviz

dot = graphviz.Digraph("pipeline", comment="ETL stages")
dot.node("extract", "Extract")
dot.node("transform", "Transform")
dot.node("load", "Load")
dot.edge("extract", "transform")
dot.edge("transform", "load")

print(dot.source)

Nothing runs the dot binary yet. Building and printing .source works even on a machine with no Graphviz installed, which makes it the safe thing to unit test.

Render to PNG or PDF on diskrender-to-a-file

import graphviz

dot = graphviz.Digraph(format="png")
dot.edge("a", "b")

path = dot.render("build/graph", cleanup=True)
print(path)  # build/graph.png

# or name the output file directly:
dot.render(outfile="build/graph.svg")

render() writes the .gv source file next to the image and leaves it there unless you pass cleanup=True. Passing outfile= infers the format from the extension and overrides format=.

Get the image bytes without touching diskrender-in-memory

import graphviz

dot = graphviz.Digraph()
dot.edge("a", "b")

png_bytes = dot.pipe(format="png")
svg_text = dot.pipe(format="svg", encoding="utf-8")

return_response(png_bytes, content_type="image/png")

pipe() returns bytes unless you pass encoding, then it returns str. This is the call to use in a web handler; render() would litter the server's filesystem.

Fail clearly when the dot executable is absenthandle-missing-binary

import graphviz

try:
    graphviz.version()
except graphviz.ExecutableNotFound:
    raise SystemExit(
        "Graphviz is not installed. Try: apt-get install graphviz"
    )

try:
    dot.render("out/graph")
except graphviz.CalledProcessError as exc:
    print("dot rejected the source:", exc.stderr)

ExecutableNotFound means the binary is missing; CalledProcessError means dot ran and complained, usually about bad DOT syntax in a label you built from user data.

Style the whole graph, or one elementset-graph-node-edge-attributes

import graphviz

dot = graphviz.Digraph(
    graph_attr={"rankdir": "LR", "bgcolor": "white"},
    node_attr={"shape": "box", "style": "rounded,filled", "fillcolor": "#eef"},
    edge_attr={"arrowhead": "vee"},
)
dot.node("err", "Failure", fillcolor="#fdd")
dot.edge("ok", "err", color="red", label="retry")

# change defaults partway through:
dot.attr("node", shape="ellipse")
dot.node("end", "Done")

dot.attr('node', ...) changes the default for every node added after that line, not the ones already added. Order matters here in a way that trips people up.

Group nodes into a boxed clustercluster-subgraphs

import graphviz

dot = graphviz.Digraph()
with dot.subgraph(name="cluster_api") as c:
    c.attr(label="API tier", style="filled", color="lightgrey")
    c.node("web")
    c.node("auth")

with dot.subgraph(name="cluster_db") as c:
    c.attr(label="Storage")
    c.node("postgres")

dot.edge("web", "postgres")

The name must start with the literal prefix cluster_ or Graphviz draws no box at all. This is an upstream DOT rule, not a Python one, and it fails silently.

Switch layout engine for non-hierarchical graphschoose-layout-engine

import graphviz

# dot: layered, good for DAGs. neato/fdp: spring layouts for messy graphs.
g = graphviz.Graph(engine="neato")
g.edge("a", "b")
g.edge("b", "c")
g.edge("c", "a")
g.render("out/mesh", format="svg")

# one-off override without rebuilding the object:
g.render("out/mesh_fdp", engine="fdp")

graphviz.Graph is undirected and writes -- edges; Digraph writes ->. Mixing an engine that ignores rankdir (like neato) with rankdir=LR silently does nothing.

Stop user text from being read as DOT markupescape-untrusted-labels

import graphviz

user_label = "<b>not html</b> | pipe \\l backslash"

dot = graphviz.Digraph()
dot.node("n1", graphviz.escape(user_label))

# opposite direction: keep a label literal that starts with '<'
dot.node("n2", graphviz.nohtml("<config>"))

Labels wrapped in angle brackets are parsed as HTML-like markup and backslash sequences such as \\l and \\N are layout directives, so unescaped user input either breaks the render or quietly changes the drawing.

Draw record nodes and attach edges to fieldsrecord-nodes-with-ports

import graphviz

s = graphviz.Digraph(node_attr={"shape": "record"})
s.node("users", "{users|<id> id\\l|<email> email\\l}")
s.node("orders", "{orders|<id> id\\l|<user_id> user_id\\l}")
s.edge("orders:user_id", "users:id")

The <name> markers define ports, and you reference them as node:port in edges. Curly braces flip the record from horizontal to vertical, which is what makes it look like a table.

Render DOT text produced somewhere elserender-existing-dot-source

import graphviz
from sklearn import tree

dot_text = tree.export_graphviz(clf, out_file=None, filled=True)
src = graphviz.Source(dot_text)
src.render("out/tree", format="pdf", cleanup=True)

# or straight from a file on disk
src = graphviz.Source.from_file("deps.gv")

Source is a thin wrapper around the text. It cannot add or remove nodes; if you need to edit the graph, parse it with pydot instead of doing regex work on the string.

Show a graph inline in a notebookdisplay-in-jupyter

import graphviz

graphviz.set_jupyter_format("png")  # default is svg

dot = graphviz.Digraph()
dot.edge("a", "b")
dot  # last expression in the cell renders inline

SVG is sharper but some notebook viewers and PDF exports drop it, so switching to png is the usual fix when a shared notebook shows blank output.

Unflatten a wide, shallow graphreduce-graph-width

import graphviz

dot = graphviz.Digraph()
for i in range(12):
    dot.edge("root", f"leaf{i}")

narrow = dot.unflatten(stagger=3)
narrow.render("out/narrow", format="png")

unflatten shells out to the separate unflatten binary and returns a new Source object, so it needs that tool present too and does not modify the original graph.

Alternatives

PackageRegistryPick it when
pydotPyPIYou need to read existing DOT files into an editable object model, not only write new ones.
pygraphvizPyPIYou want layout computed in-process through the Graphviz C library and access to the resulting node coordinates, and you can live with building a C extension.
networkxPyPIThe graph is a data structure you need to analyse and traverse, with drawing as a side task.
diagramsPyPIYou want cloud architecture pictures with vendor icons and would rather not write DOT attributes yourself.