Python Tutorial
SQLAlchemy Core and ORM
Writing SQL by hand is fine for small scripts, but larger applications benefit from SQLAlchemy, the most popular Python database toolkit. It has two layers: Core, a Pythonic way to build and run SQL against any database, and the ORM (Object-Relational Mapper), which maps database tables to Python classes so you work with objects instead of rows.
The same code runs on SQLite, PostgreSQL, MySQL, SQL Server and Oracle — only the connection URL changes. This lesson uses the modern SQLAlchemy 2.0 style with type-annotated models. FastAPI, Flask and many other frameworks are commonly paired with SQLAlchemy.
SQLAlchemy is not available in the browser runner; install it with pip install sqlalchemy and run the examples locally. They use SQLite, so no server is needed.
Engine, Connection and Session
create_engine(url) creates an engine that manages a pool of connections. URLs look like sqlite:///app.db, postgresql+psycopg://user:pass@host/db or mysql+pymysql://user:pass@host/db. With Core you use engine.connect() / engine.begin() and text() or select(). With the ORM you use a Session: it tracks the objects you add or change and writes them to the database when you commit().
Declaring Models
Models subclass a DeclarativeBase. Each attribute is annotated with Mapped[type] and optionally configured with mapped_column() (primary keys, lengths, uniqueness, foreign keys). relationship() links models, for example one author to many books, with back_populates keeping both sides in sync. Base.metadata.create_all(engine) creates the tables; in real projects, use Alembic migrations to evolve the schema.
Querying
Build queries with select(Model).where(...).order_by(...).limit(...) and run them with session.scalars(query) (model objects) or session.execute(query) (rows). .one(), .first() and .all() fetch results; session.get(Model, id) loads by primary key. Joins and aggregates use .join(), .group_by() and func.count() / func.sum(). Values are always sent as bound parameters, so SQLAlchemy queries are safe from SQL injection.
Examples
SQLAlchemy Core: engine, text() and bound parameters
# pip install sqlalchemy
from sqlalchemy import create_engine, text
engine = create_engine("sqlite:///:memory:")
# PostgreSQL: create_engine("postgresql+psycopg://user:password@localhost:5432/shop")
# MySQL: create_engine("mysql+pymysql://user:password@localhost:3306/shop")
with engine.begin() as conn: # begin() commits at the end of the block
conn.execute(text("CREATE TABLE stores (city TEXT, revenue INTEGER)"))
conn.execute(text("INSERT INTO stores VALUES (:city, :revenue)"), [
{"city": "Pune", "revenue": 820000},
{"city": "Nashik", "revenue": 310000},
{"city": "Nagpur", "revenue": 540000},
])
with engine.connect() as conn:
result = conn.execute(text("SELECT city, revenue FROM stores WHERE revenue > :minimum ORDER BY revenue DESC"),
{"minimum": 400000})
for row in result:
print(row.city, row.revenue)
Pune 820000
Nagpur 540000
ORM models, relationships and full CRUD
from sqlalchemy import create_engine, select, func, String, ForeignKey
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship, Session
class Base(DeclarativeBase):
pass
class Author(Base):
__tablename__ = "authors"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(100), unique=True)
books: Mapped[list["Book"]] = relationship(back_populates="author", cascade="all, delete-orphan")
def __repr__(self):
return f"Author({self.name!r})"
class Book(Base):
__tablename__ = "books"
id: Mapped[int] = mapped_column(primary_key=True)
title: Mapped[str] = mapped_column(String(200))
price: Mapped[int]
author_id: Mapped[int] = mapped_column(ForeignKey("authors.id"))
author: Mapped[Author] = relationship(back_populates="books")
def __repr__(self):
return f"Book({self.title!r}, {self.price})"
engine = create_engine("sqlite:///:memory:")
Base.metadata.create_all(engine)
with Session(engine) as session:
# Create
asha = Author(name="Asha", books=[Book(title="Python Basics", price=450),
Book(title="Python Testing", price=599)])
ravi = Author(name="Ravi", books=[Book(title="Deep Python", price=899)])
session.add_all([asha, ravi])
session.commit()
print("ids:", asha.id, ravi.id)
# Read
book = session.scalars(select(Book).where(Book.title == "Deep Python")).one()
print(book, "by", book.author.name)
# Update: change the attribute, then commit
book.price = 799
session.commit()
cheap = session.scalars(select(Book).where(Book.price < 700).order_by(Book.price)).all()
print(cheap)
stats = session.execute(
select(Author.name, func.count(Book.id), func.sum(Book.price))
.join(Author.books)
.group_by(Author.name)
.order_by(Author.name)
).all()
print(stats)
# Delete: the cascade removes Asha's books too
session.delete(asha)
session.commit()
print(session.scalars(select(Book)).all())
ids: 1 2
Book('Deep Python', 899) by Ravi
[Book('Python Basics', 450), Book('Python Testing', 599)]
[('Asha', 2, 1049), ('Ravi', 1, 799)]
[Book('Deep Python', 799)]
A session factory, get(), and rolling back on errors
from sqlalchemy import create_engine, String
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, sessionmaker
class Base(DeclarativeBase):
pass
class User(Base):
__tablename__ = "users"
id: Mapped[int] = mapped_column(primary_key=True)
email: Mapped[str] = mapped_column(String(255), unique=True)
engine = create_engine("sqlite:///:memory:")
Base.metadata.create_all(engine)
SessionLocal = sessionmaker(bind=engine) # create once, reuse everywhere
def register(email):
with SessionLocal() as session:
try:
user = User(email=email)
session.add(user)
session.commit()
return f"registered {email} as user {user.id}"
except IntegrityError:
session.rollback()
return f"{email} is already registered"
print(register("asha@example.com"))
print(register("asha@example.com"))
with SessionLocal() as session:
print(session.get(User, 1).email, session.get(User, 99))
registered asha@example.com as user 1
asha@example.com is already registered
asha@example.com None
Schema migrations with Alembic
pip install alembic
alembic init migrations # creates alembic.ini and migrations/
# set sqlalchemy.url in alembic.ini and target_metadata = Base.metadata in migrations/env.py
alembic revision --autogenerate -m "create authors and books"
alembic upgrade head # apply all migrations
alembic downgrade -1 # undo the last one
INFO [alembic.runtime.migration] Running upgrade -> 3f2a1c9d8e7b, create authors and books
Common Mistakes
- Mixing the legacy 1.x query API (session.query) with 2.0-style select() without understanding the difference.
- Forgetting session.commit(), so changes are lost when the session closes.
- Triggering the "N+1 queries" problem by accessing relationships in a loop — use selectinload() or joinedload().
- Using create_all() to change existing tables; it only creates missing tables — use Alembic migrations.
- Sharing one Session across threads or web requests instead of one session per unit of work.
Key Points to Remember
- SQLAlchemy Core builds and runs SQL; the ORM maps tables to classes.
- create_engine(url) works with SQLite, PostgreSQL, MySQL and more — only the URL changes.
- Declare models with DeclarativeBase, Mapped[...] and mapped_column(); link them with relationship().
- Query with select(...) and session.scalars()/execute(); commit to save changes.
- Use sessionmaker for sessions and Alembic for schema migrations.
Practice the examples
Change an input, predict the result, then compare it with the output. Explain why the result changes.
Use your local project environment for these examples. Codelab currently runs Python and HTML/CSS/JavaScript; framework examples may need project dependencies.