Vellum is done enough to ship. Here’s an honest assessment of what worked, what didn’t, and what I’d do differently.
What We Won
Zero-String Queries
The flagship feature. Instead of:
{"price": {"$gte": 10}}
You write:
Product.price >= 10
Full IDE autocomplete. Automatic refactoring. No runtime typos. This alone justifies the project for large codebases.
Composable Expression Trees
Queries are built as Python objects, not assembled from strings:
filters = []
if min_price:
filters.append(Product.price >= min_price)
if category:
filters.append(Product.category == category)
results = await repo.find(*filters)
Consistent API Across Contexts
The same FieldRef syntax works in queries, sorting, indexes, aggregations, and updates. One mental model for everything.
Gradual Adoption
Mixed usage works — typed expressions alongside raw MongoDB dictionaries. You can migrate an existing codebase incrementally without a big-bang rewrite.
What We Lost
Metaclass Fragility
The custom metaclass depends on Pydantic v2’s internal field storage mechanisms. A future Pydantic version could break this silently. This is the biggest long-term risk.
No $ Property Syntax
Python syntax rules prevent Order.$total. Aggregation field references require going through resolve_agg_refs() rather than direct attribute access.
Two Index APIs
Because class-body scoping prevents FieldRef objects during class creation, we ended up with both Settings.indexes and __init_indexes__. Two patterns for the same thing is a learning curve.
Hook Gaps
Lifecycle hooks only fire through the repository layer. Access MongoDB directly and they’re silently skipped — a footgun for teams mixing abstraction levels.
Development Distribution
| Area | Effort |
|---|---|
| Core engine (FieldRef, metaclass, expression trees) | ~40% |
| Pydantic + Motor integration | ~35% |
| Documentation and polish | ~25% |
When to Use Vellum
Good fit:
- Multiple developers working on the same models
- Frequent schema changes
- Large model ecosystems where typos are expensive
Bad fit:
- Small codebases where the overhead isn’t worth it
- Sync-only requirements (Vellum is async-first)
- Migrating legacy systems with thousands of existing raw queries