A minimal Python ORM for MariaDB/MySQL and SQLite. Made for humans.
5
stars
5
commits
Python
primary language
Sep 3, 2026
updated
A minimal Python ORM for MariaDB/MySQL and SQLite. Explicit, predictable, and made for humans.
Many ORMs try to abstract SQL away entirely, introducing their own query languages and complex concepts that force you to spend time learning “their way” before getting productive.
Model takes a different approach: it embraces native type definitions and SQL instead of hiding them behind unnecessary abstractions.
Column method has 73 typing @overloads.)

python -m pip install model-py
Note: You import the package as
model
Read the documentation at https://model.elis.cc
A model represents a database table. You just need to define the database instance, table name and columns.
Create example file: ./models/user.py
from model import Model
from model.database import SQLiteDatabase
db = SQLiteDatabase("./example.db") # this could be MySQLDatabase
class User(Model):
table = "user"
db = db
id: int = Model.Column(
type="INT",
index="PRIMARY",
auto_increment=True,
)
email: str = Model.Column(
type="VARCHAR",
length=255,
index="UNIQUE",
can_be_null=False,
)
age: int | None = Model.Column(type="INT")
Create model.config.yaml in the project root:
include_dirs:
- ./models
The model sync CLI is the easiest way to 'sync' models with the database. It automatically generates SQL diffs based on your model definitions. To keep it safe, there are some restrictions in place for column deletion and renaming.
Run CLI commands from your project root so Model can find the configuration.
Preview the generated schema change:
model sync check
If the SQL looks correct, apply it:
model sync apply
Run model sync check once more. It should report nothing left to apply.
That's it!
Model instances always represent persisted database records. Loading, inserting, updating, and querying are explicit operations, so it's easy to understand exactly what your code is doing - no complex object lifecycle, no ambiguous save().
insert() creates the row and returns a loaded model instance.
from models.user import User
user = User.insert({
"email": "john.doe@example.com",
"age": 30
})
print(user.id, user.email)
# prints: 1, john.doe@example.com
Constructing a model loads an existing row.
The primary key is automatically inferred for the model class initialization:

from models.user import User
user = User(id=1)
print(user.id, user.email)
# prints: 1, john.doe@example.com
It raises ModelRecordNotFoundError when no row matches.
Find records with SQL conditions. SQLite uses ?
for parameter placeholders (while MySQL uses %s):
from models.user import User
user = User.find_one("email = ?", ["john.doe@example.com"])
assert user is not None
print(user.id, user.email) # prints: 1, john.doe@example.com
all_users = User.find_all("age > 24 ORDER BY age")
count = User.count("email LIKE ?", ["%@example.com"])
print(all_users) # prints: [User(...)]
print(count) # prints: 1
find_one() returns None when there is no match, while find_all() returns
an empty list.
Records are updated explicitly; assign new values through update().
from models.user import User
user = User(id=1)
print(user.email)
# prints: john.doe@example.com
user.update({
"email": "johnny@example.com"
})
print(user.email)
# prints: johnny@example.com
user.delete()
This deletes the row in the database. After deletion, that instance can no longer be used for record operations.
5 commits
Python
100.0%
A minimal Python ORM for MariaDB/MySQL and SQLite. Made for humans.
5
stars
5
commits
Python
primary language
Sep 3, 2026
updated
A minimal Python ORM for MariaDB/MySQL and SQLite. Explicit, predictable, and made for humans.
Many ORMs try to abstract SQL away entirely, introducing their own query languages and complex concepts that force you to spend time learning “their way” before getting productive.
Model takes a different approach: it embraces native type definitions and SQL instead of hiding them behind unnecessary abstractions.
Column method has 73 typing @overloads.)

python -m pip install model-py
Note: You import the package as
model
Read the documentation at https://model.elis.cc
A model represents a database table. You just need to define the database instance, table name and columns.
Create example file: ./models/user.py
from model import Model
from model.database import SQLiteDatabase
db = SQLiteDatabase("./example.db") # this could be MySQLDatabase
class User(Model):
table = "user"
db = db
id: int = Model.Column(
type="INT",
index="PRIMARY",
auto_increment=True,
)
email: str = Model.Column(
type="VARCHAR",
length=255,
index="UNIQUE",
can_be_null=False,
)
age: int | None = Model.Column(type="INT")
Create model.config.yaml in the project root:
include_dirs:
- ./models
The model sync CLI is the easiest way to 'sync' models with the database. It automatically generates SQL diffs based on your model definitions. To keep it safe, there are some restrictions in place for column deletion and renaming.
Run CLI commands from your project root so Model can find the configuration.
Preview the generated schema change:
model sync check
If the SQL looks correct, apply it:
model sync apply
Run model sync check once more. It should report nothing left to apply.
That's it!
Model instances always represent persisted database records. Loading, inserting, updating, and querying are explicit operations, so it's easy to understand exactly what your code is doing - no complex object lifecycle, no ambiguous save().
insert() creates the row and returns a loaded model instance.
from models.user import User
user = User.insert({
"email": "john.doe@example.com",
"age": 30
})
print(user.id, user.email)
# prints: 1, john.doe@example.com
Constructing a model loads an existing row.
The primary key is automatically inferred for the model class initialization:

from models.user import User
user = User(id=1)
print(user.id, user.email)
# prints: 1, john.doe@example.com
It raises ModelRecordNotFoundError when no row matches.
Find records with SQL conditions. SQLite uses ?
for parameter placeholders (while MySQL uses %s):
from models.user import User
user = User.find_one("email = ?", ["john.doe@example.com"])
assert user is not None
print(user.id, user.email) # prints: 1, john.doe@example.com
all_users = User.find_all("age > 24 ORDER BY age")
count = User.count("email LIKE ?", ["%@example.com"])
print(all_users) # prints: [User(...)]
print(count) # prints: 1
find_one() returns None when there is no match, while find_all() returns
an empty list.
Records are updated explicitly; assign new values through update().
from models.user import User
user = User(id=1)
print(user.email)
# prints: john.doe@example.com
user.update({
"email": "johnny@example.com"
})
print(user.email)
# prints: johnny@example.com
user.delete()
This deletes the row in the database. After deletion, that instance can no longer be used for record operations.
5 commits
Python
100.0%