Typed Mongo
A production-ready modern Python MongoDB ODM
In addition to synchronous mode, you can use asynchronous mode, just export from typedmongo.asyncio.
Install
Requires CPython 3.10 or newer. CI covers CPython 3.10 through 3.15.
pip install typedmongo
Usage
Usage examples trump all usage documentation. So please look at the Example below first.
Example
import datetime
from typing import Literal
from pymongo import AsyncMongoClient as MongoClient
import typedmongo.asyncio as mongo
class Wallet(mongo.Document):
balance: mongo.DecimalField
class User(mongo.MongoDocument):
name: mongo.StringField
gender: mongo.LiteralField[Literal["m", "f"]]
age: mongo.IntegerField
tags: mongo.ListField[str]
wallet: mongo.EmbeddedField[Wallet]
created_at: mongo.DateTimeField = mongo.DateTimeField(
default=lambda: datetime.datetime.now(datetime.timezone.utc)
)
children: mongo.ListField[User]
extra: mongo.DictField = mongo.DictField(default=dict)
# Optional fields
nickname: mongo.OptionalField[str]
bio: mongo.OptionalField[str]
async def main():
await mongo.initial_collections(
MongoClient().mongo,
User,
)
# Insert one document
document_id = await User.objects.insert_one(
User.load(
{
"name": "Aber",
"gender": "m",
"age": 18,
"tags": ["a", "b"],
"wallet": {"balance": 100},
"children": [],
},
)
)
# Find one document
user = await User.objects.find_one(User._id == document_id, sort=[+User.age])
# Update one document
update_result = await User.objects.update_one(
User._id == document_id, {"$set": {"tags": ["a", "b", "e", "r"]}}
)
# Delete one document
delete_result = await User.objects.delete_one(User._id == document_id)
# Find one and update
user = await User.objects.find_one_and_update(
User._id == document_id, {"$set": {"tags": ["a", "b", "e"]}}
)
# Find one and replace
user = await User.objects.find_one_and_replace(
User._id == document_id,
User.load({"name": "Aber", "age": 0}),
after_document=True,
)
# Find one and delete
user = await User.objects.find_one_and_delete(User._id == document_id)
# Find many documents and sort
users = [user async for user in User.objects.find(User.age == 18, sort=[-User.age])]
# Update many documents
update_result = await User.objects.update_many(
User.wallet._.balance == Decimal("100"), {"$inc": {"wallet.balance": 10}}
)
# Count documents
await User.objects.count_documents(User.age >= 0)
# Bulk write operations
await User.objects.bulk_write(
mongo.DeleteOne(User._id == 0),
mongo.DeleteMany(User.age < 18),
mongo.InsertOne(User.load({"name": "InsertOne"}, partial=True)),
mongo.ReplaceOne(User.name == "Aber", User.load({}, partial=True)),
mongo.UpdateMany({}, {"$set": {"age": 25}}),
mongo.UpdateMany(User.name == "Yue", {"$set": {"name": "yue"}}),
)
Document
Note: The Document must be initialized with initial_collections before it can be used.
Document.load: Load data from dict to instance, and validate the data.Document.dump: Dump the instance to jsonable dict.
Document.load uses msgspec for validation
and type conversion. Unknown fields are ignored. With partial=True, missing
required fields are allowed, including in embedded documents and lists; defaults
are still applied. Each document candidate in a union must validate as a complete
document before it can be selected, even when the outer load is partial.
Embedded values may be dictionaries or existing Document instances. Existing
instances are validated against their current values on each Document.load
call, including inside optional fields, lists, and unions; constructing an
instance directly does not bypass validation during loading.
Invalid input raises typedmongo.ValidationError, a ValueError subclass.
Validation collects errors across fields, embedded documents, and list items.
The exception exposes:
messages: errors grouped by field name and list indexdata: the input being validatedvalid_data: successfully converted values from that input
Catch the public exception when handling validation failures:
from typedmongo import ValidationError
try:
user = User.load(data)
except ValidationError as exc:
print(exc.messages)
Union fields preserve exact input types when possible, then try candidate types
in declaration order until one succeeds. dump() produces JSON values, while
to_mongo() retains BSON-compatible types such as ObjectId, datetime, and
Decimal.
Collection Name
Document.__collection_name__: Normally, subclasses of Document will generate a collection_name based on the Class Name, but if you want to customize it, you can set __collection_name__ when defining it.
class APIKey(mongo.Document):
__collection_name__ = "api_key"
Raw Collection
If you want to use functions such as aggregate, you can access pymongo's original collection object through Document.objects.collection.
Document.objects.collection.aggregate([
{"$group": {"_id": "$field", "count": {"$sum": 1}}}
])
Field
ObjectIdFieldStringFieldIntegerFieldFloatFieldBooleanFieldDecimalFieldDateTimeFieldDictFieldEmbeddedFieldListFieldLiteralFieldUnionFieldEnumFieldOptionalField[T]- Optional field that defaults to None
By default, lists of embedded documents or union values may be omitted without a default.
Other list fields require a value unless they have a default or are loaded with
partial=True.
Values supplied to Document.load are validated and converted before field
load hooks run. Non-partial defaults also pass through these hooks; partial
defaults are applied by the document constructor.
Field descriptors can be used as dictionary and set keys. Binding a field to a document does not change its hash. Reusing a field as a list element preserves the storage name and query path of a directly bound field.
Fields can also be constructed directly. For example, use a union field inside a list to accept either embedded document type:
class Left(mongo.Document):
left: mongo.StringField
class Right(mongo.Document):
right: mongo.IntegerField
class Parent(mongo.Document):
items = mongo.ListField(mongo.UnionField(Left | Right))
Optional Fields
Use mongo.OptionalField[T] to declare fields that:
- Accept
Nonevalues - Default to
Nonewhen missing, in both partial and non-partial loads - Are omitted from
dump()/to_mongo()when their value isNone
class User(mongo.Document):
name: mongo.StringField # Required
nickname: mongo.OptionalField[str] # Optional, defaults to None
age: mongo.OptionalField[int]
bio: mongo.OptionalField[str]
wallet: mongo.OptionalField[Wallet] # Optional embedded document
tags: mongo.OptionalField[list[str]] # Optional list
# Create with only required fields
user = User(name="Alice")
assert user.nickname is None
# Load without optional fields (non-partial)
user = User.load({"name": "Bob"})
assert user.nickname is None
# Load without optional fields (partial)
user = User.load({"name": "Charlie"}, partial=True)
assert user.nickname is None
Difference from allow_none=True:
- Regular fields with
allow_none=Truestill require a value (or explicit default) OptionalField[T]fields automatically default toNoneif missing, in both partial and non-partial loads
Regular fields can explicitly default to None with a factory when
allow_none=True, including embedded documents and lists:
class Profile(mongo.Document):
age: mongo.IntegerField = mongo.IntegerField(
allow_none=True, default=lambda: None
)
These regular fields retain their None values in dump() and to_mongo().
Conditional expressions
If you want to use conditional expressions with methods like aggregate, you can call expression.compile() to get a mongo expression.
Document.objects.collection.aggregate([
{"$match": (Document.age >= 18).compile()},
{"$group": {"_id": "$field", "count": {"$sum": 1}}},
])
Comparison expressions
Document.field == valueDocument.field != valueDocument.field > valueDocument.field >= valueDocument.field < valueDocument.field <= value
Logical expressions
(Document.field == value) & (Document.field == value)(Document.field == value) | (Document.field == value)~(Document.field == value)~((Document.field == value) & (Document.field == value))~((Document.field == value) | (Document.field == value))
String matching shortcuts
Contains(value, case_sensitive=True)- Check if field contains substringStartsWith(value, case_sensitive=True)- Check if field starts with prefixEndsWith(value, case_sensitive=True)- Check if field ends with suffix
from typedmongo.asyncio import Contains, StartsWith, EndsWith
# Find users whose name contains "John"
users = User.objects.find(User.name == Contains("John"))
# Case-insensitive search
users = User.objects.find(User.name == Contains("john", case_sensitive=False))
# Starts with / Ends with
users = User.objects.find(User.name == StartsWith("A"))
users = User.objects.find(User.email == EndsWith("@example.com"))
RawExpression
Sometime, you maybe need use raw query, you can use RawExpression to do that.
from typedmongo.asyncio import RawExpression
# Or `from typedmongo import RawExpression`
User.objects.find(RawExpression({"field_name": {"$mongo_command": value}}) & User.age > 18)
Sort expressions
+Document.field: Ascending-Document.field: Descending
User.objects.find(..., sort=[+User.age, -User.name])
Objects
Document.objects: The object manager of theDocument.collection: The collection of theDocument.use_session: Use session for the operations. (Usecontextvars, so you don't need to pass the session to the function parameters)use_transaction: Use transaction for the operations.insert_one: Insert one document.insert_many: Insert many documents.find: Find many documents.find_one: Find one document.find_one_and_update: Find one and update.find_one_and_replace: Find one and replace.find_one_and_delete: Find one and delete.delete_one: Delete one document.delete_many: Delete many documents.update_one: Update one document.update_many: Update many documents.count_documents: Count documents.bulk_write: Bulk write operations.
Metadata
Release files for typedmongo 1.21.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| typedmongo-1.21.0.tar.gz | 73.0 kB | With provenance | Details for typedmongo-1.21.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| typedmongo-1.21.0-py3-none-any.whl | Python 3 | none | any | Details for typedmongo-1.21.0-py3-none-any.whl |
Total release size: 111.7 kB
Release files / typedmongo-1.21.0.tar.gz
| Download URL | typedmongo-1.21.0.tar.gz |
|---|---|
| Size | 73.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
4bb65c2e493f617ec384c6e604694e4a3e6feb94ec6b9c76dd83865bf8f11251
|
|
BLAKE2b-256 checksum How to use checksums |
69e95ac999507ae2bc6bd9fea7cee157a64c2872a75cf59129376edd73a01616
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 2, 2026.
Transparency logRelease files / typedmongo-1.21.0-py3-none-any.whl
| Download URL | typedmongo-1.21.0-py3-none-any.whl |
|---|---|
| Size | 38.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
75e11b62dea52ea6e75ab2c38e23decb17e30e07b70f8d78004041fe4aaaa058
|
|
BLAKE2b-256 checksum How to use checksums |
7284bc64eef26787fec5a603cbd6b0b05ed3b3142dad797241c40caba7cd9f73
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 2, 2026.
Transparency logSecurity for typedmongo 1.21.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for typedmongo-1.21.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 2, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.