SQLObject
SQLObject is easily the oldest project in this collection, originally created in 2002, but still being actively developed and released. It supports a very wide range of databases, and early in its lifetime supported many common Python ORM behaviors we might take for granted now—like using Python classes and objects to describe database tables and fields, and providing high levels of abstraction for those activities.
With most ORMs, by default, changes to objects are only reflected in the underlying database when you save or sync. SQLObject reflects object changes immediately in the database, unless you alter that behavior in the table object's definition.
Table definitions in SQLObject use custom types to describe fields—for example, StringCol()
to define a string field, and ForeignKey()
for a reference to another table. For joins, you can use a MultipleJoin()
attribute to get a table's one-to-many back references, and RelatedJoin()
for many-to-many relationships.
A handy sqlmeta
class gives you more control over a given table's programmatic behaviors—for instance, if you want to provide your own custom algorithm for how Python class names are translated into database table names, or a table's default ordering.
The querying syntax is similar to other ORMs, but not always as elegant. For instance, an OR query across two fields would look like this:
User.select(OR(User.status=="Active", User.rank=="Admin"))
A whole slew of custom query builder methods are available for performing different kinds of join operations, which is useful if you explicitly want, say, a FULLOUTERJOIN
instead of a NATURALRIGHTJOIN
.
SQLObject has little in the way of utilities. Its biggest offering there is the ability to dump and load database tables to and from CSV. However, with some additional manual work, its native admin tool lets you record versions of your database's schema and perform migrations; the upgrade process is not automatic.
Tortoise ORM
Tortoise ORM is the youngest project profiled here, and the only one that is asynchronous by default. That makes it an ideal companion for async web frameworks like FastAPI, or applications built on asynchronous principles, generally.
Creating models with Tortoise follows roughly the same pattern as other Python ORMs. You subclass Tortoise's Model
class, and use field classes like IntField
, ForeignKeyField
, or ManyToManyField
to define fields and their relationships. Models can also have a Meta
inner class to define additional details about the model, such as indexes or the name of the created table. For relationship fields, such as OneToOne
, the field definition can also specify delete behaviors such as a cascading delete.
Queries in Tortoise do not track as closely to SQL syntax as some other ORMs. For instance, User.filter(rank="Admin")
is used to express a SELECT/WHERE
query. An .exclude()
clause can be used to further refine results; for example, User.filter(rank="Admin").exclude(status="Disabled")
. This approach does provide a slightly more compact way to express common queries than the .select().where()
approach used elsewhere.
The Signals feature lets you specify behaviors before or after actions like saving or deleting a record. In other ORMs this would be done by, say, subclassing a model and overriding .save()
. With Tortoise, you can wrap a function with a decorator to specify a signal action, outside of the model definition. Tortoise also has a "router" mechanism for allowing reads and writes to be applied to different databases if needed. A very useful function not commonly seen in ORMs is .explain()
, which executes the database's plan explainer on the supplied query.
Async is still a relatively new presence in Python's ecosystem. To get a handle on how to use Tortoise with async web frameworks, the documentation provides examples for FastAPI, Quart, Sanic, Starlette, aiohttp
, and others. For those who want to use type annotations (also relatively new to the Python ecosystem), a Pydantic plugin can generate Pydantic models from Tortoise models, although it only supports serialization and not deserialization of those models. An external tool, Aerich, generates migration scripts, and supports both migrating to newer and downgrading to older versions of a schema.
Conclusion
The most widely used of the Python ORMs, SQLAlchemy, is almost always a safe default choice, even if newer and more elegant tools exist. Peewee is compact and expressive, with less boilerplate needed for many operations, but it lacks more advanced ORM features like a native mechanism for schema migrations.
Django's ORM is mainly for use with the Django web framework, but its power and feature set, especially its migration management system, make it a strong reason to consider Django as a whole. PonyORM's use of native Python metaphors makes it easy to grasp conceptually, but be aware of its opinionated defaults.
SQLObject, the oldest of the ORMs profiled here, has powerful features for evoking exact behaviors (e.g., joins), but it's not always elegant to use and has few native utilities. And the newest, Tortoise ORM, is async by default, so it complements the new generation of async-first web frameworks.