Skip to content

Getting started

Defining models

Nodes

Subclass Node and declare properties as type annotations:

import pylpg.node

class Person(pylpg.node.Node):
    name: str
    age: int | None = None

Labels default to the class name. To set custom labels:

class Employee(pylpg.node.Node):
    __labels__ = frozenset({"Person", "Employee"})
    name: str
    role: str

Relationships

Subclass Relationship with a __type__ attribute:

import pylpg.relationship

class Knows(pylpg.relationship.Relationship):
    __type__ = "KNOWS"
    since: str | None = None

Relationship descriptors

Attach traversal capabilities to nodes using descriptors:

class Person(pylpg.node.Node):
    name: str
    friends = pylpg.relationship.RelationshipTo(Knows)
    known_by = pylpg.relationship.RelationshipFrom(Knows)

Available descriptors:

  • RelationshipTo — outgoing relationships
  • RelationshipFrom — incoming relationships
  • RelationshipUndirected — undirected relationships

Connecting to a database

Neo4j

import pylpg.backend.neo4j

backend = pylpg.backend.neo4j.Neo4jBackend(
    hostname="localhost",
    port=7687,
    database="neo4j",
    username="neo4j",
    password="password",
    protocol="bolt",
)

FalkorDB

import pylpg.backend.falkordb

backend = pylpg.backend.falkordb.FalkorDBBackend(
    hostname="localhost",
    port=6379,
    database="default",
)

FalkorDBLite (embedded)

No server required:

import pylpg.backend.falkordblite

backend = pylpg.backend.falkordblite.FalkorDBLiteBackend(
    path="/tmp/my_graph.db",
    database="default",
)

Sessions

A session manages the connection between your Python objects and the database. Use it as a context manager:

import pylpg.session

with pylpg.session.Session(backend) as session:
    alice = Person(name="Alice")
    session.save(alice)

When a node is saved, it becomes bound to the session. This allows traversal and relationship operations to work without explicitly passing the session around.

CRUD operations

Creating nodes

alice = Person(name="Alice", age=30)
session.save(alice)

Updating nodes

alice.name = "Alice Smith"
session.save(alice)

Deleting nodes

session.delete(alice)

Creating relationships

Two approaches:

Direct instantiation:

relationship = Knows(source=alice, target=bob, since="2024")
session.save(relationship)  # auto-saves unsaved source/target nodes

Via descriptor:

alice.friends.connect(bob, since="2024")

Traversal

friends = alice.friends.all()
for friend in friends:
    print(friend.name)

To read the properties stored on the relationships themselves, use relationships() instead of all():

for relationship in alice.friends.relationships():
    print(relationship.target.name, relationship.since)

Each relationship carries its properties, its source and its target, and is already saved, so it can be modified and saved again:

relationship = alice.friends.relationships()[0]
relationship.since = "2025"
session.save(relationship)

all() and relationships() are separate queries whose results are not necessarily in the same order, so they must not be paired up by position.

Batch operations

Save multiple items at once using session.save(list):

nodes = [Person(name=f"person_{i}") for i in range(1000)]
session.save(nodes)

Batch save also works with relationships and automatically saves unsaved referenced nodes:

relationships = [
    Knows(source=alice, target=bob),
    Knows(source=alice, target=carol),
]
session.save(relationships)

Each backend implements batch operations optimally:

  • Neo4j — uses UNWIND queries in a single transaction for atomicity and performance
  • FalkorDB / FalkorDBLite — uses individual queries (FalkorDB does not support multi-query transactions)

Raw queries

Execute Cypher queries directly:

results = session.execute_query(
    "MATCH (n:Person) WHERE n.age > $min_age RETURN n",
    parameters={"min_age": 25},
)

Use resolve_nodes=True to automatically hydrate driver node objects into Python instances:

results = session.execute_query(
    "MATCH (n:Person) RETURN n",
    resolve_nodes=True,
)
for row in results:
    person = row["n"]  # a Person instance
    print(person.name)

Property types

Allowed property types are:

  • Primitives: str, int, float, bool, None
  • Lists of primitives: list[str], list[int], etc.
  • Unions of the above: str | None, int | float, etc.

Properties with default values are optional. Properties without defaults are required:

class Person(pylpg.node.Node):
    name: str                    # required
    age: int | None = None       # optional, defaults to None
    score: float = 0.0           # optional, defaults to 0.0