Version 7.0.0 2026-08
- Add an optional Pydantic bridge (neomodel.contrib.pydantic, install with neomodel[pydantic]): generate a Pydantic v2 model from a node class for validation/serialization/JSON-schema export (to_pydantic_model / pydantic_schema), convert a node instance to a Pydantic instance (to_pydantic), and build an unsaved node from a Pydantic instance or mapping (from_pydantic). Available as plain functions or via the PydanticBridge mixin. Carries over required/default/choices/max_length/help_text, supports include/exclude/optional(PATCH)/element_id/db-alias options, and works with both the sync and async APIs. v1 maps scalar properties; relationship expansion is left for a follow-up.
- Node classes are now discovered lazily from the live class hierarchy instead of being pushed into a global registry as a side-effect of class definition. Practical effects: (1) redefining a class (e.g. a dev-server/Streamlit hot reload) is always allowed and the latest definition wins - config.allow_reload is deprecated and no longer needed; (2) NodeClassAlreadyDefined is no longer raised when a second, distinct class declaring the same labels is defined - the clash is reported only if a node carrying those labels is actually resolved; (3) defining a model no longer mutates global state. Relationship models are still registered explicitly when a RelationshipTo/RelationshipFrom is defined. build_class_registry() and NodeClassRegistry.register() are deprecated.
- Typed query API: NodeSet / AsyncNodeSet are now generic over the node class, so MyNode.nodes.get()/first()/all()/get_or_none() and iteration are typed as MyNode (not the base StructuredNode) for type checkers and IDEs.
- Typed properties: accessing a property on an instance is now typed as its Python value (e.g. node.some_string_property is str, some_int_property is int), instead of the property descriptor. Runtime behaviour is unchanged.
- The neomodel package now type-checks cleanly under mypy, and a mypy job (plus a static typing-assertion test) runs in CI to prevent regressions. Enabled mypy's warn_unused_ignores and no_implicit_reexport, removed dead type: ignore comments, and added an explicit all to neomodel/init.py so the public API re-exports type-check cleanly.
- BREAKING: config.DATABASE_URL no longer defaults to bolt://neo4j:foobarbaz@localhost:7687. A connection must now be configured explicitly (config.DATABASE_URL, config.DRIVER, NEOMODEL_DATABASE_URL, or db.set_connection); running a query with no connection configured raises a clear error instead of silently connecting with default credentials
- Make the Neo4j driver process-wide instead of context-local: it is now shared across threads and async tasks rather than rebuilt per context, fixing a connection-pool leak (notably in thread pools and sync Celery/gunicorn workers) and ensuring close_connection() closes the driver for the whole process. Session, transaction, impersonation, parallel-runtime and target-database-name state remain context-local. set_connection() now closes the previous neomodel-managed driver before replacing it (user-supplied drivers are left untouched). Removed the unused _pid attribute.
- Refactor the Database/AsyncDatabase god object into focused collaborators behind an unchanged facade: a standalone node-class registry (neomodel/_node_class_registry.py), a ConnectionManager (driver/url/version/transactions), a QueryRunner (cypher_query/streaming/object resolution) and a SchemaManager (index/constraint installation and schema admin). The public db/adb API is unchanged. This is an internal-only change.
- create() now creates all nodes in a single UNWIND query instead of one round-trip per node, significantly speeding up bulk creation. Behaviour and return order are unchanged.
- Add StructuredNode.bulk_save(nodes) to persist a list of node instances in at most two round-trips: new nodes are inserted in a single UNWIND ... CREATE query and already-saved nodes are updated in a single UNWIND ... MATCH ... SET query, instead of one save() per node. Runs pre_save/post_save hooks on each node (not post_create) and returns the same instances in order, with created ones carrying their element_id.
- Fix unsaved-node guard
- Register only a class's base label set instead of pre-materialising every base+optional-label combination (which was exponential in the number of optional_labels); the combinations are now resolved lazily at lookup. Resolution behaviour is unchanged.
- StructuredNode instances are hashable again (usable in sets and as dict keys): defining eq had set hash to None. Saved nodes hash by element_id (consistent with equality), unsaved nodes by identity.
- Replace the assert-based runtime checks in the connection manager (begin()/commit()/rollback() and server-version detection) with explicit raises (RuntimeError), so the guards no longer vanish under
python -O. Transaction/session cleanup in commit()/rollback() is now guarded with explicit None-checks so it can never mask the original error.