Now liveThe Skillselion MCP - thousands of ranked skills, loaded into your agent mid-task. No install.Get it →
neo4j-contrib avatar

Neo4j Driver Python Skill

  • 426 installs
  • 101 repo stars
  • Updated August 3, 2026
  • neo4j-contrib/neo4j-skills

neo4j-driver-python-skill is a Python backend skill that teaches correct Neo4j Python Driver v6 patterns for sessions, transactions, async FastAPI lifespan, errors, and type mapping for developers shipping production gra

About

neo4j-driver-python-skill guides Python developers through the official Neo4j Python Driver v6 for production graph apps. It covers driver installation and singleton lifecycle with verify_connectivity, URI schemes and auth for Aura, bolt, bearer, and Kerberos, plus execute_query with RoutingControl and result_transformer_. Managed transactions via execute_read and execute_write include retry safety, result lifetime rules, and @unit_of_work; implicit session.run covers LOAD CSV and CALL {} IN TRANSACTIONS. AsyncGraphDatabase patterns support FastAPI lifespan hooks and asyncio.gather, with explicit handling for ConstraintError and related exceptions. Use it when wiring real Python services to Neo4j, not for Cypher-only query writing without driver code.

  • Driver v6.x lifecycle: singleton GraphDatabase, verify_connectivity, Aura/bolt/Kerberos auth options
  • execute_query with RoutingControl, result_transformer_, and trailing-underscore API conventions
  • Managed execute_read/execute_write vs implicit session.run for LOAD CSV and IN TRANSACTIONS batches
  • AsyncGraphDatabase with FastAPI lifespan and asyncio.gather patterns
  • Error taxonomy: ConstraintError, ServiceUnavailable, TransientError, GQL status codes plus UNWIND batch writes and bookm

Neo4j Driver Python Skill by the numbers

  • 426 all-time installs (skills.sh)
  • +35 installs in the week ending Aug 4, 2026 (Skillselion tracking)
  • Ranked #137 of 911 Databases skills by installs in the Skillselion catalog
  • Security screen: LOW risk (skills.sh audit)
  • Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/neo4j-contrib/neo4j-skills --skill neo4j-driver-python-skill

Add your badge

Show developers this skill is listed on Skillselion. Paste this into your README.

Listed on Skillselion
Installs426
repo stars101
Security audit3 / 3 scanners passed
Last updatedAugust 3, 2026
Repositoryneo4j-contrib/neo4j-skills

How do you use Neo4j Python Driver v6 correctly?

Implement correct Neo4j Python driver v6 patterns—sessions, transactions, async FastAPI lifespan, errors, and type mapping—in production graph apps.

Who is it for?

Python backend developers integrating Neo4j Aura or self-hosted instances into FastAPI or asyncio services with production-grade transaction patterns.

Skip if: Developers who only need Cypher query syntax help without Python driver lifecycle, transaction management, or async integration code.

When should I use this skill?

The user asks about Neo4j Python driver, execute_query, execute_read, AsyncGraphDatabase, FastAPI Neo4j lifespan, or ConstraintError handling.

What you get

Singleton driver setup, execute_query calls, managed read/write transactions, async FastAPI lifespan hooks, and typed error handling.

  • driver singleton setup
  • transaction wrappers
  • async lifespan hooks

By the numbers

  • Targets official Neo4j Python Driver v6 API patterns

Files

SKILL.mdMarkdownGitHub ↗

When to Use

  • Writing Python code that connects to Neo4j
  • Setting up driver, sessions, transactions, or async patterns
  • Debugging result handling, serialization, or UNWIND batching
  • Reviewing Neo4j driver usage in Python code

When NOT to Use

  • Writing/optimizing Cypherneo4j-cypher-skill
  • Driver version upgradesneo4j-migration-skill
  • GraphRAG pipelines (neo4j-graphrag package) → neo4j-graphrag-skill

---

Installation

pip install neo4j                  # package name is `neo4j`, NOT `neo4j-driver` (deprecated since v6)
pip install neo4j-rust-ext         # optional: 3–10× faster serialization, same API

Python >=3.10 required for v6.x.

---

Environment Variables

Load connection config from environment — never hardcode credentials.

import os
from dotenv import load_dotenv   # pip install python-dotenv

load_dotenv(".env")   # reads NEO4J_URI / NEO4J_USERNAME / NEO4J_PASSWORD / NEO4J_DATABASE

URI      = os.getenv("NEO4J_URI",      "neo4j://localhost:7687")
USER     = os.getenv("NEO4J_USERNAME", "neo4j")
PASSWORD = os.getenv("NEO4J_PASSWORD", "")
DATABASE = os.getenv("NEO4J_DATABASE", "neo4j")

.env file format:

NEO4J_URI=neo4j+s://xxx.databases.neo4j.io
NEO4J_USERNAME=neo4j
NEO4J_PASSWORD=secret
NEO4J_DATABASE=neo4j

Add .env to .gitignore. Without python-dotenv, use export in shell or os.getenv directly.

---

Driver Lifecycle

Create one Driver per application. Thread-safe, expensive to create. Never create per-request.

from neo4j import GraphDatabase

URI  = "neo4j+s://xxx.databases.neo4j.io"   # Aura
AUTH = ("neo4j", "password")

# Context manager — preferred for scripts
with GraphDatabase.driver(URI, auth=AUTH) as driver:
    driver.verify_connectivity()
    # ... work ...

# Long-lived singleton (service / web app)
driver = GraphDatabase.driver(URI, auth=AUTH)
driver.verify_connectivity()
# on shutdown:
driver.close()

URI schemes:

SchemeUse
neo4j+s://TLS + cluster routing — Aura default
neo4j://Unencrypted + cluster routing
bolt+s://TLS, single instance
bolt://Unencrypted, single instance

Auth options: ("user", "pass") tuple, basic_auth(), bearer_auth("jwt"), kerberos_auth("b64").

---

Choosing the Right API

APIUse whenAuto-retryStreaming
driver.execute_query()Most queries — simple, safe default❌ eager
session.execute_read/write()Large results / multiple queries in one tx
session.run()LOAD CSV, CALL {} IN TRANSACTIONS, scripts⚠️ one-shot [6.2+]
AsyncGraphDatabaseasyncio applications

session.run() retry [6.2+]: single immediate retry on DBMS-marked idempotent errors only (currently admission control). Disable with disable_auto_commit_retries=True at driver or session level.

---

execute_query — Default API

from neo4j import GraphDatabase, RoutingControl

# Tuple unpacking — most common
records, summary, keys = driver.execute_query(
    "MATCH (p:Person {name: $name})-[:KNOWS]->(f) RETURN f.name AS name",
    name="Alice",
    routing_=RoutingControl.READ,   # route reads to replicas
    database_="neo4j",              # always specify — saves a round-trip
)
for record in records:
    print(record["name"])
print(summary.result_available_after, "ms")

# Write — check counters
summary = driver.execute_query(
    "CREATE (p:Person {name: $name, age: $age})",
    name="Bob", age=30,
    database_="neo4j",
).summary
print(summary.counters.nodes_created)

Trailing-underscore convention — config kwargs end with _ (database_, routing_, auth_, result_transformer_, bookmark_manager_). No query parameter name may end with _; pass those via parameters_={"key_": val}.

Never f-string or format Cypher. Always $param — prevents injection and enables plan caching.

result_transformer_ — reshape before return:

import neo4j
df      = driver.execute_query("MATCH (p:Person) RETURN p.name, p.age", database_="neo4j",
                                result_transformer_=neo4j.Result.to_df)
record  = driver.execute_query("MATCH (p:Person {name:$n}) RETURN p", n="Alice", database_="neo4j",
                                result_transformer_=neo4j.Result.single)   # raises if 0 or 2+ results

Result.single() raises ResultNotSingleError on zero results (not just 2+). Use single(strict=False) for None-on-empty.

---

Managed Transactions (execute_read / execute_write)

Use for large results or multiple queries in one transaction.

with driver.session(database="neo4j") as session:

    def get_people(tx):
        result = tx.run("MATCH (p:Person) WHERE p.name STARTS WITH $pfx RETURN p.name AS name",
                        pfx="Al")
        return [r["name"] for r in result]   # consume INSIDE callback — Result invalid after tx closes

    names = session.execute_read(get_people)

    def create_person(tx):
        tx.run("CREATE (p:Person {name: $name})", name="Carol")

    session.execute_write(create_person)

Result lifetimeResult is a lazy cursor backed by the open transaction. Returning it unconsumed raises ResultConsumedError. Always collect to list inside the callback.

Callback may retry on transient failures — keep callbacks idempotent; move side effects (HTTP calls, emails) outside the callback.

Timeout/metadata via @unit_of_work (named functions only — cannot decorate lambdas):

from neo4j import unit_of_work

@unit_of_work(timeout=5.0, metadata={"app": "svc", "user": user_id})
def get_people(tx):
    return [r["name"] for r in tx.run("MATCH (p:Person) RETURN p.name AS name")]

session.execute_read(get_people)

---

Implicit Transactions (session.run)

Use only for LOAD CSV, CALL {} IN TRANSACTIONS, or quick scripts. session.run() does a single immediate retry on idempotent (DBMS-marked) errors only [6.2+]; other errors do not retry.

with driver.session(database="neo4j") as session:
    result = session.run("CREATE (p:Person {name: $name})", name="Alice")
    summary = result.consume()   # call consume() to guarantee commit before proceeding
    print(summary.counters.nodes_created)

# Opt out of one-shot retry [6.2+] — driver- or session-level
driver = GraphDatabase.driver(URI, auth=AUTH, disable_auto_commit_retries=True)
with driver.session(database="neo4j", disable_auto_commit_retries=True) as session:
    session.run("...")

---

Async API

Mirror of sync API — replace GraphDatabase with AsyncGraphDatabase, await every call.

from neo4j import AsyncGraphDatabase
import asyncio

# Singleton — same rule as sync: never create per-request
driver = AsyncGraphDatabase.driver(URI, auth=AUTH)

async def main():
    records, _, _ = await driver.execute_query(
        "MATCH (p:Person) RETURN p.name AS name",
        database_="neo4j", routing_=RoutingControl.READ,
    )
    print([r["name"] for r in records])
    await driver.close()

asyncio.run(main())

FastAPI lifespan pattern:

from contextlib import asynccontextmanager
from fastapi import FastAPI

_driver = None

@asynccontextmanager
async def lifespan(app: FastAPI):
    global _driver
    _driver = AsyncGraphDatabase.driver(URI, auth=AUTH)
    await _driver.verify_connectivity()
    yield
    await _driver.close()

app = FastAPI(lifespan=lifespan)

Parallel queries with asyncio.gather:

results = await asyncio.gather(
    driver.execute_query("MATCH (a:Artist) RETURN a.name AS name", database_="neo4j"),
    driver.execute_query("MATCH (v:Venue)  RETURN v.name AS name",  database_="neo4j"),
)

Never use sync `GraphDatabase` in asyncio — blocks the event loop.

Full async patterns → references/async.md

---

Error Handling

from neo4j.exceptions import (
    Neo4jError, ServiceUnavailable, TransientError,
    AuthError, ConstraintError,
)

try:
    driver.execute_query("...", database_="neo4j")
except AuthError:
    ...  # bad credentials
except ServiceUnavailable:
    ...  # no servers reachable
except ConstraintError as e:
    # unique/existence constraint violation — catch BEFORE Neo4jError (it's a subclass)
    print(e.code, e.message)
except TransientError as e:
    # raised only after retries exhausted (execute_query retries automatically)
    print(e.code)
except Neo4jError as e:
    print(e.code, e.message, e.gql_status)

Catch ConstraintError before Neo4jError — it is a subclass and will be swallowed otherwise.

---

Result Access & Null Safety

record = records[0]
record["name"]               # by key — KeyError if absent
record[0]                    # by index
record.get("name")           # None for absent key OR graph null
record.get("name", "Unknown")
d = record.data()            # dict — values still driver objects for Node/Rel/temporal types

record.data() is not JSON-safe if result contains Node, Relationship, Path, or neo4j.time.* values. Project scalar fields in Cypher instead of returning whole nodes.

# ❌ raises TypeError on json.dumps
records, _, _ = driver.execute_query("MATCH (p:Person) RETURN p", database_="neo4j")
json.dumps(records[0].data())

# ✅ project scalars
records, _, _ = driver.execute_query(
    "MATCH (p:Person) RETURN p.name AS name, p.age AS age", database_="neo4j")
json.dumps(records[0].data())   # safe

Node/Relationship/temporal access:

node = record["p"]           # neo4j.graph.Node
node.element_id              # stable within this transaction only
node.labels                  # frozenset({'Person'})
dict(node)                   # all properties as plain dict

rel  = record["r"]           # neo4j.graph.Relationship
rel.type                     # 'KNOWS'

dt = record["created_at"]    # neo4j.time.DateTime
dt.to_native()               # datetime.datetime (loses sub-µs precision)

Full type mapping table → references/data-types.md

---

Batch Writes with UNWIND

Pass list[dict] — only shape the driver serializes correctly for UNWIND.

people = [{"name": "Alice", "age": 30}, {"name": "Bob", "age": 25}]
driver.execute_query(
    "UNWIND $rows AS row MERGE (p:Person {name: row.name}) SET p.age = row.age",
    rows=people,
    database_="neo4j",
)

Custom objects and dataclasses must be converted to dict before passing as parameters.

---

Performance

  • Always set database_ / database= — omitting triggers a home-database round-trip per call.
  • execute_read routes to replicas automatically; use routing_=RoutingControl.READ with execute_query.
  • Batch writes: one execute_write callback for the whole list > one tx per item.
  • Large results: stream lazily inside execute_read callback; execute_query is always eager.

Connection pool tuning:

driver = GraphDatabase.driver(URI, auth=AUTH,
    max_connection_pool_size=50,        # default 100
    connection_acquisition_timeout=30,  # seconds to wait for free connection
    max_connection_lifetime=3600,       # seconds; recycles stale connections
    connection_timeout=15,
    keep_alive=True,
)

Session exhaustion: each open session holds a connection. Always use with driver.session(...) as session.

Full performance patterns → references/performance.md

---

Common Errors

MistakeFix
f-string / .format() Cypher paramsUse $param placeholders always
Param name ending with _Pass via parameters_={"key_": val}
Omitting database_Always set — saves a round-trip every call
Returning Result from tx callbackConsume to list inside callback
Side effects in execute_read/write callbackMove outside — callback may retry
Passing dataclass/Pydantic as paramConvert to dict first
UNWIND with list of objectslist[dict] only
record.get() for absent-key detection"key" in record.keys() for absent; .get() returns None for both absent and graph null
No .consume() after session.run()Commit timing undefined; call .consume()
Sync driver inside asyncioUse AsyncGraphDatabase — sync blocks event loop
Async driver created per requestSingleton — create once at startup
Leaked sessionswith driver.session(...) as session always
json.dumps(record.data()) with node/temporalProject scalars in Cypher or convert explicitly
result["name"] on EagerResultIndex result.records[0]["name"] or unpack records, _, _ = ...
Result.single() returns None for 0 resultsIt raises — use single(strict=False)
@unit_of_work on lambdaUse named function
Neo4jError caught before ConstraintErrorCatch ConstraintError first — it's a subclass
neo4j-driver package namePackage is neo4j since v6; neo4j-driver deprecated

---

References

Load on demand:

  • references/async.md — full async patterns: managed transactions, result methods, concurrency
  • references/data-types.md — complete Python↔Cypher type mapping, temporal conversion, graph object API, spatial types (CartesianPoint/WGS84Point)
  • references/performance.md — connection pool, lazy streaming, threading vs asyncio, bookmarks/causal consistency
  • references/transactions.md — explicit transactions, rollback, commit uncertainty, unit_of_work details

Docs:

  • https://neo4j.com/docs/python-manual/current/
  • https://neo4j.com/docs/api/python-driver/current/

---

Checklist

  • [ ] Package installed as neo4j (not neo4j-driver)
  • [ ] One Driver instance created at startup; shared everywhere
  • [ ] verify_connectivity() called at startup
  • [ ] database_ / database= set on every call
  • [ ] $param placeholders used — no f-strings or .format()
  • [ ] Result consumed inside tx callback (not returned raw)
  • [ ] Sessions used as context managers (with driver.session(...) as session)
  • [ ] ConstraintError caught before Neo4jError
  • [ ] AsyncGraphDatabase used in asyncio code (not sync driver)
  • [ ] Async driver created once at app startup (not per request)
  • [ ] Side effects outside execute_read/write callbacks
  • [ ] UNWIND batches use list[dict]

Related skills

How it compares

Use neo4j-driver-python-skill over generic database skills when you need Neo4j-specific transaction semantics, routing control, and async Python driver lifecycle—not SQL ORM patterns.

FAQ

Which Neo4j Python Driver version does neo4j-driver-python-skill target?

neo4j-driver-python-skill targets the official Neo4j Python Driver v6, covering execute_query as the default API, managed transactions, implicit session.run, and AsyncGraphDatabase patterns for production Python graph apps.

Does neo4j-driver-python-skill cover FastAPI integration?

Yes. neo4j-driver-python-skill documents AsyncGraphDatabase with a FastAPI lifespan pattern, asyncio.gather for concurrent queries, plus singleton driver setup and verify_connectivity for production services.

Is Neo4j Driver Python Skill safe to install?

skills.sh reports 3 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.

Databasesbackendintegrations

This week in AI coding

Five minutes, every Monday - the tools, releases and tactics for developers.

unsubscribe anytime.