01.11.2025 • 2 min read

Vellum Part 3: What We Won, What We Lost

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

AreaEffort
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

View the Vellum source on GitHub