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)

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