(graphs)-[:ARE]->(everywhere)
So put them in your python!
Exploring Graph Data Types in Modern Python
Dom Weldon
PyCon FR
Samedi 31 Octobre 2026 - 16h30
BSky/GitHub
@DomWeldon
dom.weldon@gmail.com
Les graphes sont partout, alors mettez-les dans votre python!
Une exploration de les types de données en python pour les graphes
Dom Weldon
Ingénieur Logiciel
Dom Weldon
Ingénieur Logiciel
Ingénieur Logiciel
Fullstack
Platform Engineer
Ingénieur Platforme
Dom Weldon
Ingénieur Logiciel
Ingénieur Logiciel
Fullstack
Dom Weldon
Based in
Central London
Platform Engineer
Dom Weldon
Based in
Central London
Contracting for Financial Services
Previous: Consultancy
Past: Academic
Now: Investment Management
Platform Engineer
“Graphs” / « Graphes »
A
C
B
Graph(e)s
Charts / « Graphiques »
Pairwise relationships between entities.
Graph Theory: key terms
A
C
B
G = (V, E)
V = { A, B, C }
E = { (A, B), (B, C), (C, A) }
Graphs model pairwise relationships between entities.
They can be defined as a pair:
edges / arcs
arêtes / liens
nodes / vertices
nœuds / sommets
Types of Graph
A
C
B
A
C
B
A
B
A
B
Undirected
Directed
MultiGraph
Directed
MultiGraph
Types of Graph
A
C
B
A
C
B
A
B
A
B
Undirected
Directed
MultiGraph
Directed
MultiGraph
Cyclical
Types of Graph
A
C
B
A
C
B
A
B
A
B
Undirected
Directed
MultiGraph
Directed
MultiGraph
Cyclical
Acyclical
Graphs are matrices, and vice versa
| UK | IE | FR | ES | PT | AD | CH | DE | BE |
UK | | 499 | | | | | | | |
IE | 499 | | | | | | | | |
FR | | | | 646 | | 55 | 525 | 418 | 556 |
ES | | | 646 | | 1224 | 63 | | | |
PT | | | | 1224 | | | | | |
AD | | | 55 | 63 | | | | | |
CH | | | 525 | | | | | 348 | |
DE | | | 418 | | | | 348 | | 133 |
BE | | | 556 | | | | | 133 | |
Sparse matrix of length of land borders between selected European countries.
Graphs are matrices, and vice versa
| UK | IE | FR | ES | PT | AD | CH | DE | BE |
UK | | 499 | | | | | | | |
IE | 499 | | | | | | | | |
FR | | | | 646 | | 55 | 525 | 418 | 556 |
ES | | | 646 | | 1224 | 63 | | | |
PT | | | | 1224 | | | | | |
AD | | | 55 | 63 | | | | | |
CH | | | 525 | | | | | 348 | |
DE | | | 418 | | | | 348 | | 133 |
BE | | | 556 | | | | | 133 | |
Property Graph Model
A way to model data as a graph, where:
A
C
B
name: “A”
colour: yellow
name: “B”
colour: green
name: “C”
colour: purple
x: true
d: [1,2,3]
x: false
d: [4,5,6]
x: false
xyz: -2.5
(graphs)-[:ARE]-(everywhere)
This is a fun slogan, written in Cypher, a graph query language.
But, it has a deeper meaning.
Graphs are the most fundamental way to understand the world around us¹.
Modelling data well as a graph allows us to write better, more declarative code, that has positive effects across a whole code base.
CREATE (fr:Country {
Name: “France”,
ShortName: “fr”
})-[:BORDERS {
Length_km: 646
}]->(es:Country {
Name: “Spain”,
ShortName: “es”,
});
¹ Disagree? Fight me.
Graphs in Python
Python does not have an intrinsic graph data type.
Instead we have to use libraries. In the remainder of this talk we’ll explore two libraries:
Lots of discussion of graphs in python focuses around doing network analysis, which is not specifically the focus of this talk.
Focus is on using graphs as a data type in your everyday engineering-focused code, and why doing this is a Good Thing™ for your codebase.
Graphs in Python
Python does not have an intrinsic graph data type.
Instead we have to use libraries. In the remainder of this talk we’ll explore two libraries:
Lots of discussion of graphs in python focuses around doing network analysis, which is not specifically the focus of this talk.
Focus is on using graphs as a data type in your everyday engineering-focused code, and why doing this is a Good Thing™ for your codebase.
Using Graphs in Python: NetworkX
Most established and widely used set of tools for working with graph data types in python.
“Pandas for graphs”
$ uv add networkx types-networkx
Main package
Separately managed and bundled set of type stubs, providing reasonably good coverage for most use cases
(type hints for graphs are limited by the constraints of the current typing system, but PEPs 827 and 849 have the potential to make much more powerful use cases possible in typed graphs!)
NetworkX: the basics
A
C
B
A
C
B
A
B
A
B
nx.Graph()
nx.DiGraph()
nx.MultiGraph()
nx.MultiDiGraph()
import networkx as nx
G = nx.Graph()
NetworkX: the basics
>>> import networkx as nx
>>> G = nx.DiGraph()
>>> G.add_node("A", colour="yellow", name="A")
>>> G.nodes
['A']
>>>G.nodes["A"]
{'colour': 'yellow', 'name': 'A'}
>>> G.add_edge("A", "B", x=False, d=[4,5,6])
>>> G.nodes
NodeView(('A', 'B'))
>>> G.edges
OutEdgeView([('A', 'B')])
>>> G.edges[("A", "B")]
{'x': False, 'd': [4, 5, 6]}
>>> G["A"]["B"]
{'x': False, 'd': [4, 5, 6]}
>>> G.edges["A", "B"]["y"] = {1: 2}
A
C
B
name: “A”
colour: yellow
name: “B”
colour: green
name: “C”
colour: purple
x: true
d: [1,2,3]
x: false
d: [4,5,6]
x: false
xyz: -2.5
Nodes can be any hashable object.
They have a set of properties that you can access via subscription on G.nodes. These are dict[str, Any].
Edges are added similarly, and also have properties.
You can access edges either via G.edges or using a double subscription on G.
All properties are mutable
NetworkX: the basics
>>> import networkx as nx
>>> G = nx.DiGraph()
>>> G.add_node("A", colour="yellow", name="A")
>>> print(G.nodes)
['A']
>>> print(G.nodes["A"])
{'colour': 'yellow', 'name': 'A'}
>>> G.add_edge("A", "B", x=False, d=[4,5,6])
>>> G.nodes
NodeView(('A', 'B'))
>>> G.edges
OutEdgeView([('A', 'B')])
>>> G.edges[("A", "B")]
{'x': False, 'd': [4, 5, 6]}
>>> G["A"]["B"]
{'x': False, 'd': [4, 5, 6]}
>>> G.edges["A", "B"]["y"] = {1: 2}
A
C
B
name: “A”
colour: yellow
name: “B”
colour: green
name: “C”
colour: purple
x: true
d: [1,2,3]
x: false
d: [4,5,6]
x: false
xyz: -2.5
Nodes can be any hashable object.
They have a set of properties that you can access via subscription on G.nodes. These are dict[str, Any].
Edges are added similarly, and also have properties.
You can access edges either via G.edges or using a double subscription on G.
All properties are mutable
The NetworkX documentation and code refers to these values on nodes, edges, etc. as ‘attributes’ or ‘attrs’, but they are never accessed like attributes - i.e., through the getattr(obj, name) pattern - only ever via subscription, hence why I (correctly) call them properties.
NetworkX: the basics
import networkx as nx
G = nx.DiGraph()
G.add_nodes_from([
(
"A",
{
"name": "A",
"colour": "yellow"
},
),
(
"B",
{
"name": "B",
"colour": "green",
}
),
(
"C",
{
"name": "C",
"colour": "purple"
}
),
])
G.add_edges_from((
("A", "B", {"x": False, "d": [4,5,6]}),
("B", "C", {"x": False, "xyz": -2.5}),
("A", "C", {"x": True, "d": [1,2,3]}),
)
A
C
B
name: “A”
colour: yellow
name: “B”
colour: green
name: “C”
colour: purple
x: true
d: [1,2,3]
x: false
d: [4,5,6]
x: false
xyz: -2.5
>>> G.nodes
NodeView(('A', 'B', 'C'))
>>> G.edges
OutEdgeView([('A', 'B'), ('A', 'C'), ('B', 'C')])
>>> {n: d for n, d in G.nodes(data=True)}
{'A': {'name': 'A', 'colour': 'yellow'},
'B': {'name': 'B', 'colour': 'green'},
'C': {'name': 'C', 'colour': 'purple'},
}
>>> {(u, v): d for u, v, d in G.edges(data=True)}
{('A', 'B'): {'x': False, 'd': [4, 5, 6]},
('A', 'C'): {'x': True, 'd': [1, 2, 3]},
('B', 'C'): {'x': False, 'xyz': -2.5}}
NetworkX
Uses various systems-language backends for more complex analysis features on large graphs, but has a stable pure-python implementation that is good for most everyday use.
Includes visualisation features, primarily aimed at network analysis functionality.
RustworkX
A different data model: nodes and edges are both referenced by indices, which are integers.
Nodes and edges have data payloads, which can be of any python type.
Data is accessed by NodeIndices or EdgeIndices.
Graphs are MultiGraphs by default.
A
C
B
A
C
B
A
B
A
B
rx.PyGraph(
multigraph=False
)
rx.PyDiGraph(
multigraph=False
)
rx.PyGraph()
rx.PyDiGraph()
import rustworkx as rx
G = rx.PyGraph()
RustworkX: the basics
A
C
B
A
C
B
A
B
A
B
rx.PyGraph(
multigraph=False
)
import rustworkx as rx
G = rx.PyGraph()
rx.PyDiGraph(
multigraph=False
)
rx.PyGraph()
rx.PyDiGraph()
graphlib
Example Use: Database Query
Focus on some real-world examples of using graphs in code.
Scenario:
class Category(TableBase, table=True):
id: int = sqlmodel.Field(primary_key=True)
name: str
class Product(TableBase, table=True):
id: int = sqlmodel.Field(primary_key=True)
category: int = sqlmodel.Field(foreign_key="category.id")
name: str
price: float
class User(TableBase, table=True):
id: int = sqlmodel.Field(primary_key=True)
email: str
class Order(TableBase, table=True):
id: int = sqlmodel.Field(primary_key=True)
user_id: int = sqlmodel.Field(primary_key=True, foreign_key="user.id")
class OrderProducts(TableBase, table=True):
order_id: int = sqlmodel.Field(primary_key=True, foreign_key="order.id")
product_id: int = sqlmodel.Field(primary_key=True, foreign_key="product.id")
quantity: int
Constructing Graphs: choosing nodes
Important first step is generally deciding what types to use for a node. I use frozen pydantic models, with a function identify(obj)
import typing
import pydantic
type IdentityKind = typing.Literal["Table"]
class Identity(pydantic.BaseModel):
kind: IdentityKind
model_config = pydantic.ConfigDict(
frozen=True,
)
class Identifiable[IT: Identity](typing.Protocol):
def __dw_identify__(self) -> IT: ...
def identify[IT: Identity](obj: Identifiable[IT]) -> IT:
return obj.__dw_identify__()
By ensuring that nodes have a well defined type, you can create an ID to reference another node easily.
Provides a single interface to identify an object to put in a graph.
Using Graphs: identifying nodes
import typing
import pydantic
type IdentityKind = typing.Literal["Table"]
class Identity(pydantic.BaseModel):
kind: IdentityKind
model_config = pydantic.ConfigDict(
frozen=True,
)
class Identifiable[IT: Identity](typing.Protocol):
def __dw_identify__(self) -> IT: ...
def identify[IT: Identity](obj: Identifiable[IT]) -> IT:
return obj.__dw_identify__()
base.py
class TableIdentity(base.Identity):
kind: base.IdentityKind = "Table"
name: str
class TableBase(sqlmodel.SQLModel):
__table__: typing.ClassVar[sqlmodel.Table]
@classmethod
def __dw_identify__(cls) -> TableIdentity:
return TableIdentity(
name=cls.__table__.name
)
class Category(TableBase, table=True):
id: int = sqlmodel.Field(primary_key=True)
name: str
tables.py
Using a protocol allows easy expansion for multiple types of node.
class TableIdentity(base.Identity):
kind: base.IdentityKind = "Table"
name: str
class TableNodeData(typing.TypedDict):
table: typing.NotRequired[sqlmodel.Table]
model: typing.NotRequired[TableBase]
class TableEdgeData(typing.TypedDict):
foreign_key: typing.NotRequired[sqlmodel.ForeignKey]
attr: typing.NotRequired[str]
target_attr: typing.NotRequired[str]
class TableGraph[
_TI: TableIdentity = TableIdentity,
_TND: TableNodeData = TableNodeData,
_TED: TableEdgeData = TableEdgeData,
](nx.DiGraph): ...
We can also type our graphs using NetworkX, but the exact behaviour varies according to your type checker, and requires more recent python functionality (3.14/5).
def sqlmodels_to_G(sqlmodels: typing.Iterable[type[TableBase]]) -> TableGraph:
"""Construct a TableGraph from an iterable of SQLModel objects"""
G = TableGraph()
for model in sqlmodels:
table: sqlmodel.Table = model.__table__
G.add_node(
meta.identify(model),
table=table,
model=model,
)
for fk in table.foreign_keys:
G.add_edge(
identify(model),
TableIdentity(
name=fk.target_tokens.table_name,
),
foreign_key=fk,
attr=fk.parent.name,
target_attr=(fk.column.name)
)
return G
Add nodes using our identifiers, give properties to individual nodes and edges.
Taking tree-like data from a set of SQLModel/SQLAlchemy objects and turning them into a more native graph-structure.
G = tables.sqlmodels_to_G(tables.ALL_TABLES)
>>> G.nodes
{
TableIdentity(kind='Table',
name='product'),
TableIdentity(kind='Table',
name='order'),
TableIdentity(kind='Table',
name='orderproducts'),
TableIdentity(kind='Table',
name='category'),
TableIdentity(kind='Table',
name='user')
}
>>> rich.print({(u, v): d for u, v, d in G.edges(data=True)})
{
( TableIdentity(kind='Table', name='product'),
TableIdentity(kind='Table', name='category')): {
'foreign_key': ForeignKey('category.id'),
'attr': 'category',
'target_attr': 'id'
},
( TableIdentity(kind='Table', name='order'),
TableIdentity(kind='Table', name='user')): {
'foreign_key': ForeignKey('user.id'),
'attr': 'user_id',
'target_attr': 'id'
},
( TableIdentity(kind='Table', name='orderproducts'),
TableIdentity(kind='Table', name='order')): {
'foreign_key': ForeignKey('order.id'),
'attr': 'order_id',
'target_attr': 'id'
},
( TableIdentity(kind='Table', name='orderproducts'),
TableIdentity(kind='Table', name='product')): {
'foreign_key': ForeignKey('product.id'),
'attr': 'product_id',
'target_attr': 'id'
}
}
Constructed our graph containing data about our schema.
Using Graphs
def query_planner[TB: TableBase = TableBase](
models: typing.Iterable[TB],
G_tables: TableGraph,
) -> typing.Iterable[TB]:
"""Sort objects in a topologically safe order"""
for gen in list(nx.topological_generations(G_tables))[::-1]:
yield from (
model
for model in models
if identify(model.__class__) in gen
)
Use topological sorting to ensure that no model will be inserted before any other model it has a foreign key pointing to.
Graphs as declarative code
Being a software engineer is about much more than writing code.
We should aim to discover as much as possible about the real-world processes we build software for, and to record this information as thoroughly as possible in our code.
Graphs are a powerful tool to allow us to do that.
Being able to understand your problem, and express it as a graph, affords lots of rich possibilities to tackle your business problems and build on them for the future.
Only stdlib tool for working with graphs.
Implements a topological sorter and nothing else.
Originally intended for demonstration purposes for pure python implementations of the method resolution order.
(graphs)-[:ARE]->(everywhere)
So put them in your python!
Exploring Graph Data Types in Modern Python
Dom Weldon
PyCon FR
Samedi 31 Octobre 2026 - 16h30
BSky/GitHub
@DomWeldon
dom.weldon@gmail.com
Les graphes sont partout, alors mettez-les dans votre python!
Une exploration de les types de données en python pour les graphes