Lightodm Minimalistic Mongodb Odm
LightODM: A Lightweight MongoDB ODM for Python
Why I Built This
I’ve been using Beanie for MongoDB projects, and it’s genuinely a great ODM—well-designed, feature-rich, and solid for production use. But on several projects, I kept running into the same friction points:
- I needed both sync and async in the same codebase (FastAPI endpoints + background workers)
- The custom query builder felt like learning a new language when I already knew MongoDB’s syntax
- I wanted to drop down to raw PyMongo/Motor when needed, without fighting abstractions
- For simpler projects, the feature set felt heavier than necessary
So I built LightODM—not as a replacement for Beanie, but as a lighter alternative when you want Pydantic validation without giving up direct control over MongoDB.
What is LightODM?
It’s a minimal ODM that focuses on the essentials:
- Works with both sync (PyMongo) and async (Motor) in the same model
- Uses Pydantic v2 for validation and serialization
- Lets you use MongoDB’s native query syntax (no DSL to learn)
- Exposes the actual PyMongo/Motor collections when you need them
- Stays small (~500 lines) with just 3 dependencies: pydantic, pymongo, motor
Installation
pip install lightodm
Works with Python 3.8+.
Quick Comparison: LightODM vs Beanie
| Feature | LightODM | Beanie |
|---|---|---|
| Sync Support | ✅ Yes | ❌ Async only |
| Async Support | ✅ Yes | ✅ Yes |
| Code Size | ~500 lines | ~5000+ lines |
| Query Syntax | MongoDB native | Custom query builder |
| Pydantic | v2 | v2 |
| Core Dependencies | 3 (pydantic, pymongo, motor) | 5+ |
| Connection | Environment variables | init_beanie() required |
Use LightODM when:
- You need both sync and async operations
- You already know MongoDB query syntax and don’t want to learn a DSL
- You want to access raw PyMongo/Motor collections easily
- You prefer fewer abstractions
Use Beanie when:
- You only need async (it’s async-first by design)
- You want document relationships and links built in
- You prefer a query builder API over raw MongoDB syntax
- You need more ODM features out of the box
How It Works
Define Your Models
from lightodm import MongoBaseModel
from typing import Optional
class User(MongoBaseModel):
class Settings:
name = "users" # MongoDB collection name
name: str
email: str
age: Optional[int] = None
Connect to MongoDB
Set up your connection via environment variables:
export MONGO_URL="mongodb://localhost:27017"
export MONGO_DB_NAME="myapp"
# Optional:
export MONGO_USER="your_user"
export MONGO_PASSWORD="your_password"
That’s it—LightODM handles the connection automatically when you use your models.
Synchronous Operations
# Create and save
user = User(name="Alice", email="alice@example.com", age=28)
user.save()
# Retrieve by ID
found = User.get(user.id)
# Find with MongoDB query syntax
adults = User.find({"age": {"$gte": 18}})
# Update
User.update_one(
{"_id": user.id},
{"$set": {"age": 29}}
)
# Delete
user.delete()
Asynchronous Operations
Every method has an async version prefixed with a:
import asyncio
async def main():
# Create and save
user = User(name="Bob", email="bob@example.com")
await user.asave()
# Retrieve
found = await User.aget(user.id)
# Find
users = await User.afind({"age": {"$gte": 18}})
# Update
await User.aupdate_one(
{"_id": user.id},
{"$set": {"age": 30}}
)
# Delete
await user.adelete()
asyncio.run(main())
Same model, same interface—choose sync or async based on your context.
Key Features
1. Pydantic v2 Native Integration
Full Pydantic v2 validation and serialization:
from pydantic import EmailStr, Field
class User(MongoBaseModel):
class Settings:
name = "users"
name: str = Field(..., min_length=1, max_length=100)
email: EmailStr
age: int = Field(..., ge=0, le=150)
2. Direct MongoDB Access
No abstraction layers—use PyMongo/Motor directly:
# Get the actual collection
collection = User.get_collection()
# Use any PyMongo method
collection.create_index([("email", 1)], unique=True)
# Run aggregation pipelines
pipeline = [
{"$match": {"age": {"$gte": 18}}},
{"$group": {"_id": "$city", "count": {"$sum": 1}}}
]
results = User.aggregate(pipeline)
3. Thread-Safe Singleton Connection
Connection management with automatic cleanup:
from lightodm import MongoConnection
# Singleton pattern
conn = MongoConnection()
# Both sync and async clients
sync_client = conn.client
async_client = await conn.get_async_client()
# Automatic cleanup on exit (atexit handlers)
4. Bring Your Own Connection (BYOC)
For advanced scenarios, override connection methods:
from pymongo import MongoClient
client = MongoClient("mongodb://localhost:27017")
db = client.myapp
class User(MongoBaseModel):
class Settings:
name = "users"
name: str
@classmethod
def get_collection(cls):
return db.users # Use your own connection
Real-World Use Cases
FastAPI Integration
from fastapi import FastAPI, HTTPException
from lightodm import MongoBaseModel
app = FastAPI()
class User(MongoBaseModel):
class Settings:
name = "users"
name: str
email: str
@app.post("/users/")
async def create_user(user: User):
await user.asave()
return user
@app.get("/users/{user_id}")
async def get_user(user_id: str):
user = await User.aget(user_id)
if not user:
raise HTTPException(status_code=404, detail="User not found")
return user
Just make sure your environment variables are set before starting the app.
Multi-Tenant Applications
from contextvars import ContextVar
current_tenant: ContextVar[str] = ContextVar("current_tenant")
class TenantModel(MongoBaseModel):
@classmethod
def get_collection(cls):
tenant = current_tenant.get()
db_name = f"tenant_{tenant}"
return client[db_name][cls.Settings.name]
class User(TenantModel):
class Settings:
name = "users"
name: str
Aggregation Pipelines
# Revenue by date
pipeline = [
{
"$match": {
"created_at": {"$gte": start_date},
"status": "completed"
}
},
{
"$group": {
"_id": {"$dateToString": {"format": "%Y-%m-%d", "date": "$created_at"}},
"revenue": {"$sum": "$total"},
"count": {"$sum": 1}
}
},
{"$sort": {"_id": 1}}
]
results = await Order.aaggregate(pipeline)
Why Not Beanie?
Beanie is excellent, but it’s built for a different use case:
Beanie’s strengths:
- Rich feature set (migrations, relationships, etc.)
- Fluent query API
- Active development and community
- Great for async-only applications
LightODM’s philosophy:
- Minimalistic (easier to understand and debug)
- Sync + async support
- Direct MongoDB control
- Zero magic, maximum transparency
- Perfect for simple to medium complexity projects
Think of it this way:
- Beanie = Django ORM (comprehensive, opinionated)
- LightODM = SQLAlchemy Core (lightweight, flexible)
Migration from Beanie
Migrating is straightforward:
# Before (Beanie)
from beanie import Document
class User(Document):
name: str
email: str
class Settings:
name = "users"
await init_beanie(database=db, document_models=[User])
await user.insert()
users = await User.find(User.age >= 18).to_list()
# After (LightODM)
from lightodm import MongoBaseModel, connect
class User(MongoBaseModel):
name: str
email: str
class Settings:
name = "users"
connect(db_name="mydb")
await user.asave() # or user.save() for sync
users = await User.afind({"age": {"$gte": 18}})
Main changes:
- Change base class:
Document→MongoBaseModel - Replace
insert()withsave()orasave() - Use MongoDB query syntax instead of Beanie’s fluent API
- Handle relationships manually
See the full migration guide for details.
Project Status
- Version: 0.1.0
- License: Apache 2.0
- Repository: github.com/Aprova-GmbH/lightodm
- Documentation: Coming soon on ReadTheDocs
- Test Coverage: >95%
- Python Support: 3.11, 3.12, 3.13
Design Philosophy
LightODM follows these principles:
-
Minimalistic: Keep the codebase small (~500 lines) and focused on core ODM functionality.
-
Explicit > Implicit: No magic. Collection names are explicit. Query syntax is MongoDB’s native syntax. Connections are transparent.
-
Dual Support: Both sync and async APIs with the same model. Use what fits your context.
-
Pydantic Native: Leverage Pydantic v2 fully without compatibility layers or wrappers.
-
MongoDB Direct: Expose PyMongo/Motor collections directly. No abstraction penalty.
-
Zero Dependencies Beyond Core: Only pydantic, pymongo, and motor. No surprises in your dependency tree.
What’s Missing (By Design)
LightODM intentionally omits features to stay minimal:
❌ Migration tools - Use PyMongo directly or write your own ❌ Automatic relationships - Reference documents manually via IDs ❌ Query builder DSL - Use MongoDB’s query syntax ❌ Caching layer - Add your own if needed ❌ Event hooks - Use Pydantic validators or override methods
If you need these features, Beanie is a better choice.
Try It
pip install lightodm
Or check out the GitHub repository for examples and documentation.
If you’re building Python applications with MongoDB and want:
- Both sync and async support
- Direct MongoDB control
- Pydantic v2 validation
- Minimal abstraction
…give LightODM a try. It solves one problem—MongoDB ODM—and solves it simply.
Examples in the Wild
Check out the examples/ directory in the repository:
- Basic usage - Sync CRUD operations
- Async usage - Async operations
- FastAPI integration - Complete REST API
- Custom connection - BYOC pattern
Questions or feedback? Open an issue on GitHub or reach out at vya@aprova.ch.
Want to contribute? Check out the contributing guide.