1 The Problem
We want to build an ORM (Object-Relational Mapper): a tool that lets you work with database rows as Python objects — user.save(), User.all() — while it writes the SQL behind the scenes. It teaches metaprogramming and how ORMs like Django’s and SQLAlchemy’s actually bridge objects and tables.
2 How to Think About It
Think about mapping an object to a row, before any code:
User) maps to a table; its attributes map to columns. → 2. save() turns the object’s values into an INSERT statement. → 3. all() runs a SELECT and turns each row back into an object. → The ORM generates the SQL from the class’s field names, so you never write it.
3 The Build — explained part by part
Here is a complete mini ORM built on SQLite. Each part is explained below.
import sqlite3
class Model:
# Subclasses set `table` and `fields`.
table = None
fields = []
def __init__(self, **kwargs):
for field in self.fields:
setattr(self, field, kwargs.get(field))
@classmethod
def _db(cls):
return sqlite3.connect("app.db")
@classmethod
def create_table(cls):
columns = ", ".join(f"{f} TEXT" for f in cls.fields)
cls._db().execute(f"CREATE TABLE IF NOT EXISTS {cls.table} ({columns})")
def save(self):
# Build an INSERT from this object's field values.
placeholders = ", ".join("?" for _ in self.fields)
values = [getattr(self, f) for f in self.fields]
db = self._db()
db.execute(f"INSERT INTO {self.table} VALUES ({placeholders})", values)
db.commit()
@classmethod
def all(cls):
# SELECT every row and turn each into an object.
rows = cls._db().execute(f"SELECT * FROM {cls.table}").fetchall()
return [cls(**dict(zip(cls.fields, row))) for row in rows]
class User(Model):
table = "users"
fields = ["name", "email"]
if __name__ == "__main__":
User.create_table()
User(name="Alice", email="alice@example.com").save()
for user in User.all():
print(user.name, user.email)
table and fields.__init__ with setattr — for each declared field, set it as an attribute from the keyword arguments. So
User(name="Alice") gives the object a .name. setattr sets an attribute by its string name — this is metaprogramming.create_table — build a
CREATE TABLE statement by joining the field names into columns. The class’s fields define the table’s shape.save() — generate an
INSERT: one ? placeholder per field, with the object’s values. Placeholders (not string-joining) prevent SQL injection.all() — run a
SELECT, then zip(cls.fields, row) pairs each column name with its value, and cls(**dict(...)) rebuilds an object. Rows become objects.
? placeholders and pass values separately.cls.fields so it adapts to any model.cls(**dict(zip(fields, row))).4 Test & Prove Each Part
We test the SQL generation — that the right statements are built from the fields — using an in-memory database.
import sqlite3
class Model:
table = None
fields = []
_conn = None
def __init__(self, **kwargs):
for field in self.fields:
setattr(self, field, kwargs.get(field))
@classmethod
def _db(cls):
if Model._conn is None:
Model._conn = sqlite3.connect(":memory:")
return Model._conn
@classmethod
def create_table(cls):
columns = ", ".join(f"{f} TEXT" for f in cls.fields)
cls._db().execute(f"CREATE TABLE IF NOT EXISTS {cls.table} ({columns})")
def save(self):
placeholders = ", ".join("?" for _ in self.fields)
values = [getattr(self, f) for f in self.fields]
db = self._db()
db.execute(f"INSERT INTO {self.table} VALUES ({placeholders})", values)
db.commit()
@classmethod
def all(cls):
rows = cls._db().execute(f"SELECT * FROM {cls.table}").fetchall()
return [cls(**dict(zip(cls.fields, row))) for row in rows]
class User(Model):
table = "users"
fields = ["name", "email"]
def test_round_trip():
"""A saved object can be loaded back."""
User.create_table()
User(name="Alice", email="a@x.com").save()
users = User.all()
assert users[0].name == "Alice"
def test_fields_as_attributes():
"""Declared fields become attributes."""
u = User(name="Bob", email="b@x.com")
assert u.name == "Bob"
assert u.email == "b@x.com"
def test_all_returns_objects():
"""all() returns User objects, not tuples."""
users = User.all()
assert all(isinstance(u, User) for u in users)
Run with pytest -v. Using :memory: gives a fresh database per test run with no files. The round-trip test — save an object, load it back, check it matches — proves the whole object-to-SQL-to-object cycle works.
5 The Interface
Usage
User(name="Alice", email="a@x.com").save()Returns
[User(name="Alice", ...), ...]6 Run It & Automate It
python3 orm.pyDefine a model, save objects, and load them — with no SQL in your own code.
Jenkins runs the tests automatically — each line explained below.
Alice alice@example.comOperationalError: no such table.create_table() before saving.fields matches the INSERT order.// Jenkinsfile — automated tests on every change.
pipeline {
agent any
stages {
stage('Get the code') { steps { checkout scm } } // download the code
stage('Install') { steps { sh 'python3 -m venv venv && . venv/bin/activate && pip install pytest' } } // test tool
stage('Test') { steps { sh '. venv/bin/activate && pytest -v' } } // run every test
}
post {
success { echo 'All tests passed.' }
failure { echo 'A test failed — look above.' }
}
}
- filter(). Add
User.where(name="Alice"). (Teaches: WHERE clauses.) - Update and delete. Complete the CRUD operations. (Teaches: more SQL generation.)
- Field types. Support integers and dates, not just text. (Teaches: type mapping.)
setattr, generating SQL from class fields). You now understand how Django models and SQLAlchemy bridge objects and tables — it is classes that write their own SQL. Related: sqlite3, Object-Oriented Python.