Back to notes
PythonDatabase

Taming a Graph Database with Python

A practical Neo4j and Python field guide covering connections, CRUD operations, relationships, constraints, indexes, and batch work.

Neo4j with Python

Neo4j stores information as nodes, relationships, and properties. The official Python driver lets an application communicate with the database over Bolt while keeping Cypher queries explicit.

flowchart LR
    A["Python application"] -->|Bolt| B[("Neo4j")]
    B --> C["Nodes"]
    B --> D["Relationships"]
    C --> E["Properties"]
    D --> E

[!NOTE] These examples focus on the driver layer. In production, keep credentials in environment variables and add complete session-level error handling.

Install and connect

pip install neo4j
from neo4j import GraphDatabase

uri = "bolt://localhost:7687"
username = "neo4j"
password = "your_password"

driver = GraphDatabase.driver(uri, auth=(username, password))

Create, read, update, and delete nodes

def create_node(tx, node_id, label):
    query = "CREATE (n:Animal {custom_id: $node_id, label: $label})"
    tx.run(query, node_id=node_id, label=label)

def get_node(tx, node_id):
    query = "MATCH (n:Animal {custom_id: $node_id}) RETURN n"
    return tx.run(query, node_id=node_id).single()

def update_node(tx, node_id, new_label):
    query = """
    MATCH (n:Animal {custom_id: $node_id})
    SET n.label = $new_label
    """
    tx.run(query, node_id=node_id, new_label=new_label)

def delete_node(tx, node_id):
    query = """
    MATCH (n:Animal {custom_id: $node_id})
    DETACH DELETE n
    """
    tx.run(query, node_id=node_id)

Use explicit read and write transactions so retry and routing behavior remain predictable:

with driver.session() as session:
    session.execute_write(create_node, "animal_1", "Dog")
    record = session.execute_read(get_node, "animal_1")

Relationships

def connect_animals(tx, source_id, target_id):
    query = """
    MATCH (a:Animal {custom_id: $source_id})
    MATCH (b:Animal {custom_id: $target_id})
    CREATE (a)-[:KNOWS]->(b)
    """
    tx.run(query, source_id=source_id, target_id=target_id)

To remove a relationship without deleting either node:

def disconnect_animals(tx, source_id, target_id):
    query = """
    MATCH (a:Animal {custom_id: $source_id})
          -[r:KNOWS]->
          (b:Animal {custom_id: $target_id})
    DELETE r
    """
    tx.run(query, source_id=source_id, target_id=target_id)

Constraints and indexes

Create schema rules before loading production data:

CREATE CONSTRAINT animal_custom_id IF NOT EXISTS
FOR (n:Animal) REQUIRE n.custom_id IS UNIQUE;

CREATE INDEX animal_label IF NOT EXISTS
FOR (n:Animal) ON (n.label);

The unique constraint protects identity; the index accelerates common label lookups.

Batch creation

UNWIND keeps bulk writes in one query:

def create_many(tx, nodes):
    query = """
    UNWIND $nodes AS item
    CREATE (n:Animal {
        custom_id: item.custom_id,
        label: item.label
    })
    """
    tx.run(query, nodes=nodes)

nodes = [
    {"custom_id": "animal_1", "label": "Dog"},
    {"custom_id": "animal_2", "label": "Cat"},
    {"custom_id": "animal_3", "label": "Fox"},
]

with driver.session() as session:
    session.execute_write(create_many, nodes)

Always close the driver when the application shuts down:

driver.close()