pygraph-tool is a module to create and manipulate graphs.
Project description
pygraph-tool
pygraph-tool is a lightweight Python library to create and manipulate graphs.
It provides a simple object-oriented API for working with nodes and edges. Nodes and edges have stable identifiers, can store user-defined values, and can be annotated with metadata for filtering, categorization, layering, or application-specific usage.
Features
- Directed and bidirectional edges
- Automatic UUID-based identifiers when no id is provided
- User-defined values on nodes and edges
- Mutable node and edge values
- Optional metadata on nodes and edges
- Fast lookup by node and edge identifiers
- Successor and predecessor traversal
- Incoming, outgoing, and incident edge retrieval
- Generic filtering with predicates
- Metadata-based filtering
Installation
Once published on PyPI:
pip install pygraph-tool
With uv:
uv add pygraph-tool
Getting started
Import modules
from pygraph_tool import Graph
You can also import the main public classes:
from pygraph_tool import Edge, Metadata, Node
And the exceptions:
from pygraph_tool import EdgeException, GraphException, NodeException
Create a graph
A new graph starts empty.
graph = Graph()
Add nodes
A node stores a user-defined value. The value can be any Python object.
graph.add_node(value="I'm n1", node_id="n1")
graph.add_node(value="I'm n2", node_id="n2")
graph.add_node(value="I'm n3", node_id="n3")
If no node_id is provided, a UUID-based identifier is generated automatically:
node = graph.add_node(value="Generated id node")
print(node.node_id)
The created node is returned by add_node():
node = graph.add_node(value={"name": "Python"}, node_id="python")
print(node.node_id)
print(node.value)
If a node with the same identifier already exists, a GraphException is raised:
try:
graph.add_node(value="I'm n1 again", node_id="n1")
except GraphException as error:
print(error)
Add metadata to nodes
Metadata can be used for tags, categories, layers, flags, or custom properties.
metadata = Metadata(
tags={"python", "graph"},
categories={"concept"},
layers={"knowledge_base"},
flags={"visible"},
properties={"priority": 1},
)
graph.add_node(
value="Python graph concept",
node_id="python-graph",
metadata=metadata,
)
Add unidirectional edges
A unidirectional edge connects a start node to an end node.
graph.add_unidirectional_edge(
node_id_start="n1",
node_id_end="n2",
edge_id="e1",
weight=1.5,
)
graph.add_unidirectional_edge(
node_id_start="n3",
node_id_end="n2",
edge_id="e2",
)
graph.add_unidirectional_edge(
node_id_start="n1",
node_id_end="n3",
edge_id="e3",
)
This creates:
n1 -> n2
n3 -> n2
n1 -> n3
If no edge_id is provided, a UUID-based identifier is generated automatically:
edge = graph.add_unidirectional_edge(
node_id_start="n1",
node_id_end="n2",
)
print(edge.edge_id)
If an edge with the same identifier already exists, a GraphException is raised:
try:
graph.add_unidirectional_edge(
node_id_start="n2",
node_id_end="n3",
edge_id="e1",
)
except GraphException as error:
print(error)
Add bidirectional edges
A bidirectional edge connects two nodes in both directions.
graph.add_bidirectional_edge(
node_id_start="n2",
node_id_end="n3",
edge_id="e4",
)
This creates a logical bidirectional relationship:
n2 <-> n3
A bidirectional edge is considered both incoming and outgoing for each of its connected nodes.
Add values to edges
Edges can also store user-defined values.
This is useful when an edge represents a meaningful relationship, such as a dependency, a semantic link, a knowledge relation, or a similarity score.
edge = graph.add_unidirectional_edge(
node_id_start="n1",
node_id_end="n2",
edge_id="relation-1",
value={
"type": "supports",
"confidence": 0.85,
},
)
print(edge.value)
Edge values can be updated without removing the edge:
edge.value = {
"type": "supports",
"confidence": 0.95,
}
Add metadata to edges
edge_metadata = Metadata(
tags={"semantic", "verified"},
categories={"relation"},
layers={"knowledge_base"},
)
graph.add_unidirectional_edge(
node_id_start="n1",
node_id_end="n2",
edge_id="semantic-link",
value="supports",
metadata=edge_metadata,
)
Access nodes and edges
node = graph.get_node("n1")
edge = graph.get_edge("e1")
print(node.value)
print(edge.weight)
You can check whether a node or edge exists:
print(graph.is_node("n1"))
print(graph.is_edge("e1"))
Traverse the graph
Successors
Successors are nodes reachable from a given node.
successors = graph.get_successors("n1")
for node in successors:
print(node.node_id)
Predecessors
Predecessors are nodes that point to a given node.
predecessors = graph.get_predecessors("n2")
for node in predecessors:
print(node.node_id)
For bidirectional edges, the opposite endpoint is considered both a successor and a predecessor.
Retrieve connected edges
Outgoing edges
outgoing_edges = graph.get_outgoing_edges("n1")
for edge in outgoing_edges:
print(edge.edge_id)
Incoming edges
incoming_edges = graph.get_incoming_edges("n2")
for edge in incoming_edges:
print(edge.edge_id)
Incident edges
Incident edges are all edges connected to a node, regardless of direction.
incident_edges = graph.get_incident_edges("n2")
for edge in incident_edges:
print(edge.edge_id)
Filter nodes and edges
Filter with predicates
You can filter nodes with any custom predicate:
nodes = graph.filter_nodes(
lambda node: node.node_id.startswith("n")
)
You can also filter edges:
edges = graph.filter_edges(
lambda edge: edge.weight > 1.0
)
Filter by metadata
You can filter nodes by tags, categories, layers, and flags:
nodes = graph.filter_nodes_by_metadata(
tags=["python"],
layers=["knowledge_base"],
)
By default, all expected values within each criterion must be present.
nodes = graph.filter_nodes_by_metadata(
tags=["python", "graph"],
match_all=True,
)
Use match_all=False to require at least one value within each criterion:
nodes = graph.filter_nodes_by_metadata(
tags=["python", "java"],
match_all=False,
)
Different criteria are always combined with AND.
For example:
nodes = graph.filter_nodes_by_metadata(
tags=["python", "java"],
layers=["knowledge_base"],
match_all=False,
)
This means:
(tags contains "python" OR "java")
AND
(layers contains "knowledge_base")
Edges can be filtered the same way:
edges = graph.filter_edges_by_metadata(
tags=["semantic"],
categories=["relation"],
)
Remove edges
removed_edge = graph.remove_edge("e3")
print(removed_edge.edge_id)
If the edge does not exist, a GraphException is raised.
try:
graph.remove_edge("unknown-edge")
except GraphException as error:
print(error)
Remove nodes
Removing a node also removes all edges connected to it.
removed_node = graph.remove_node("n2")
print(removed_node.node_id)
If the node does not exist, a GraphException is raised.
try:
graph.remove_node("unknown-node")
except GraphException as error:
print(error)
Display a graph
Here is a simple display helper:
def display_graph(graph: Graph) -> None:
"""Display a simple text representation of a graph."""
print("Nodes:")
for node in graph.nodes:
print(f"- {node.node_id}: {node.value}")
print("Edges:")
for edge in graph.edges:
arrow = "<->" if edge.bidirectional else "->"
print(
f"- {edge.node_start.node_id} "
f"{arrow} "
f"{edge.node_end.node_id} "
f"({edge.edge_id}, weight={edge.weight}, value={edge.value})"
)
display_graph(graph)
Error handling
pygraph-tool exposes dedicated exceptions:
from pygraph_tool import EdgeException, GraphException, NodeException
Typical cases:
NodeException: invalid node creationEdgeException: invalid edge creationGraphException: invalid graph operation, such as duplicate identifiers or missing nodes/edges
Example:
try:
graph.get_node("missing-node")
except GraphException as error:
print(error)
Development
Install development dependencies:
uv sync --dev
Run tests:
uv run pytest
Run tests with coverage:
uv run coverage run -m pytest
uv run coverage report
Run linting and formatting:
uv run ruff check .
uv run ruff format .
Run type checking:
uv run mypy pygraph_tool
Build the package:
uv build --no-sources
Author
Created and maintained by David BEL AICH.
For questions or suggestions, please contact: belaich.david@outlook.fr.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pygraph_tool-1.0.0.tar.gz.
File metadata
- Download URL: pygraph_tool-1.0.0.tar.gz
- Upload date:
- Size: 9.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.3 {"installer":{"name":"uv","version":"0.11.3","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":null,"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
05358ffb7a3f0e5f98adad50b1c0e193dcf3607b69c26ab905469bf7de15fc15
|
|
| MD5 |
7a356cfbe061d8a35f979c4e8f0b8895
|
|
| BLAKE2b-256 |
2adf4a8e429829d3eb00513eec6185ea65718bd23d72b80352e6ae93cc170e35
|
File details
Details for the file pygraph_tool-1.0.0-py3-none-any.whl.
File metadata
- Download URL: pygraph_tool-1.0.0-py3-none-any.whl
- Upload date:
- Size: 11.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.3 {"installer":{"name":"uv","version":"0.11.3","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":null,"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
931178f1f3417984bcbf6b99ff651ac9d2d1be830ce851f8c7120e60dd78ec54
|
|
| MD5 |
aefb14593f9a1e7b7c86ddcb33db3d80
|
|
| BLAKE2b-256 |
eb39f8f451b69c3088cd0d342c098c787fa00cec21c1516b3c68089370ce9206
|