
Titus EddyMost Python apps I build need the same few things from a database layer. They need models that...
Most Python apps I build need the same few things from a database layer. They need models that validate their data, readable queries, a handful of relationships, and migrations I can trust in production. I wanted that in one small package with a short learning curve and safe defaults, so I built DoveORM.
It's open source (MIT), on PyPI at version 0.8.0, and supports SQLite, MySQL/MariaDB and PostgreSQL.
📖 Docs: https://doveorm.imaradesk.com
💻 GitHub: https://github.com/imaradesk/doveorm
pip install doveorm # SQLite needs nothing else
pip install "doveorm[postgres]" # or [mysql]
from doveorm import Model, configure, CharField, IntegerField
configure(DATABASE_PATH="app.db")
class User(Model):
name = CharField(max_length=100, nullable=False)
age = IntegerField(default=0)
User.create_table()
user = User.create(name="Ada", age=36)
user.age = 37
user.save()
adults = User.where(age__gte=18).order_by("name").get()
It works the same way in a Flask app, a FastAPI service, a background worker or a plain script. No web framework is required.
The QuerySet is lazy and chainable, uses field__lookup filters, and supports Q objects for OR, AND and NOT. For the most common lookups there are also dynamic finders:
user = User.findByUsername("ada") # first match or None
posts = Post.findAllByAuthorId(1) # list
count = User.countByActive(True) # int
found = Tag.existsByName("python") # bool
Relationships cover HasMany, HasOne, BelongsTo and ManyToMany, and eager loading avoids N+1 queries.
This was the part I cared about most. DoveORM compares your models with the migration history and writes the migration for you. The migrations are reversible and squashable, and a single doveorm command runs them:
doveorm generate # detect model changes → migrations/0001_initial.py
doveorm apply # apply everything pending
doveorm sql 0002 # print the SQL, run nothing
doveorm revert 2 # unapply the last two
doveorm verify # CI: exit 1 if a model change has no migration
doveorm squash 0007 # fold 0001…0007 into one migration
doveorm verify is the one I'd recommend to everyone. Put it in CI and a pull request can no longer change a model without the matching migration. For tables your code reads but doesn't own, such as database views or another service's tables, set migrate = False and DoveORM will leave them alone.
An ORM sits between user input and your data, so I made the safe path the default one:
limit and offset must be integers.update() or delete() is refused unless you write Model.all() explicitly.from_input() only sets fields that are editable and on the allowed list.sensitive=True fields are left out of to_dict() and redacted in error messages and query logs.sql_mode, optimistic locking, and atomic migrations.Transactions can be nested using SAVEPOINTs, and db.run_in_transaction(fn) retries automatically on deadlocks and lock timeouts. Connections are thread-safe and fork-safe.
Being clear about limits saves everyone time. DoveORM is synchronous only and supports three databases. Window functions, CTEs and heavy reporting queries are best written as raw SQL with db.execute(...). Many-to-many relationships need a join-table model you define yourself. It's also a young 0.x project, so pin the version and read the changelog before you upgrade.
If you need async or a much broader feature set today, a larger, more established ORM is the better choice. If you want something small, predictable and safe for CRUD-heavy apps, give DoveORM a try.
The docs are at doveorm.imaradesk.com, and the code and changelog are on GitHub. Issues, pull requests and blunt feedback on the API are all welcome. I'm especially interested in hearing what would stop you from using it on your next project. 👇