Skip to content

Modernization migration guide

Apply these changes in order so connection, query, and persistence behavior do not shift all at once.

1. Replace Motor with PyMongo Async

Remove motor imports and dependencies. Public native types now come from pymongo.asynchronous: AsyncMongoClient, AsyncDatabase, AsyncCollection, async cursors, and AsyncClientSession.

Mongoz requires pymongo>=4.13,<5.0, the supported native async driver line. Keep Pydantic and Pydantic Settings within their declared 2.x ranges. orjson is not a Mongoz runtime dependency.

2. Make Registry lifecycle explicit

Create one Registry per application lifecycle, not per request. First database use binds its client to that event loop. Close it asynchronously during shutdown or use async with Registry(...) for bounded work. close() is idempotent but final; remove code that reuses or expects to reopen a closed Registry.

Imports and document declaration perform no client or index I/O. Move connectivity checks and registry.document_checks() to an explicit startup hook.

3. Review immutable query reuse

Manager and QuerySet builders now clone state. A derived filter, sort, limit, projection, database, or session no longer mutates its source chain. Remove workarounds that manually reconstruct base queries and add regression tests when callers previously depended on mutation.

none() is async and returns an isolated empty query state. Await it; do not expect it to clear a shared manager.

Unknown lookup operators raise OperatorInvalid immediately. Correct misspelled or dynamically invented operators rather than relying on accidental field parsing.

4. Choose update or save semantics

Instance update(**values) is an atomic modeled $set patch and synchronizes supplied fields on the instance. save() synchronizes every modeled field and can overwrite concurrent changes to those fields. Use targeted updates for patches and save only when the instance is the intended snapshot.

Unknown modeled update keys and ambiguous no-op instance writes are rejected. Missing acknowledged instance update/save/delete operations raise DocumentNotFound. Add handling where stale instances were previously treated as successful writes.

Modeled create/save paths serialize declared fields only. If an application intentionally persists schemaless extra keys, move that work to an explicit native collection boundary.

5. Separate get_or_create predicates and values

Build lookup predicates on the query and pass creation-only values in defaults. Mongoz no longer mixes operator structures into the inserted document. Enforce uniqueness with a MongoDB unique index and handle native duplicate-key races according to application policy.

6. Migrate index startup and deletion policy

Index declaration no longer implies import-time I/O. Use plan_indexes() to inspect and check_indexes() to execute. Missing declarations are created by default; unmanaged indexes are retained.

Same-name specification changes require force_drop=True. Unmanaged deletion requires an audited plan_indexes(delete_unmanaged=True) followed by check_indexes(drop_unmanaged=True). Treat drop_indexes(force=True) as a destructive migration, not routine startup.

7. Propagate sessions explicitly

Start sessions from registry.driver. Bind query objects with using_session(session) and pass session= to instance writes, aggregation, bulk writes, and index operations. There is no implicit ambient session. Use a session sequentially; do not share it across concurrent tasks.

Transaction commit/abort and native errors remain PyMongo contracts. A replica set or sharded cluster is required.

8. Adopt aggregation and bulk boundaries

Document.aggregate(pipeline, session=...) materializes native pipeline mappings and closes its cursor. Document.bulk_write(requests, ordered=..., session=...) accepts PyMongo write models and returns BulkWriteResult. Both are trusted developer structures; native errors are preserved.

9. Correct signal receivers

Receivers must be async callables accepting keyword arguments. Registration order is dispatch order; execution is sequential and fail-fast. Duplicate registration is ignored, cancellation is preserved, and document classes own isolated broadcasters. Remove synchronous receivers and any logic that depends on unordered or background execution.

10. Update sync and exception boundaries

run_sync() accepts one awaitable and executes it exactly once. Inside a thread with a running event loop it blocks that thread while a worker thread runs the awaitable with copied context variables. Do not use it as a non-blocking async helper.

Import public Mongoz errors from mongoz or mongoz.exceptions. Catch native PyMongo errors for database-owned failures. DocumentNotFound now covers missing acknowledged instance writes as well as exact queries.

11. Harden regex and raw queries

String helpers have literal semantics and escape regex metacharacters. Use Q.pattern() only for an intentional raw regex. Treat dictionaries, raw(), raw expressions, pipelines, bulk requests, native drivers, and $where as trusted-only structures. Never forward decoded request mappings.

$where is legacy server-side JavaScript, cannot use indexes, and is deprecated by MongoDB 8.0. Migrate to ordinary operators or $expr.

12. Update tooling and proof

The repository's canonical type checker is ty, including positive and negative consumer fixtures. Supported usage is warning-free. Run static checks, the real standalone MongoDB suite, replica-set transaction proof, package/wheel validation, dependency audit, docs validation, and the supported Python matrix before release.