Python Course
Databases
Almost every real application stores data that outlives the program — user accounts, orders, logs, content. Databases are the standard solution. Python gives you two main paths: the built-in sqlite3 module for lightweight, file-based databases with no setup, and SQLAlchemy — the most widely used Python database toolkit — for working with any database engine through a clean, Pythonic interface.
This lesson covers both: raw SQL with sqlite3, the SQLAlchemy Core for flexible query building, and the SQLAlchemy ORM for mapping Python classes directly to database tables.
Relational Databases — Core Concepts
Before writing any code, a few terms worth pinning down:
- Table — stores data in rows and columns, like a spreadsheet with strict types
- Row — one record in a table
- Column — a named, typed field in every row
- Primary key — a column (usually
id) that uniquely identifies each row - Foreign key — a column that references the primary key of another table, creating a relationship
- SQL — Structured Query Language — the standard language for reading and writing relational data
SQLite with the Built-in sqlite3 Module
SQLite stores an entire database in a single file. No server, no installation, no configuration — just import and go. It is the right tool for scripts, prototypes, small apps, and local data storage.
Real-world use: mobile apps, desktop tools, data pipelines, test suites, and any project where you need a real database but not the overhead of PostgreSQL or MySQL.
# SQLite basics — create, insert, query, update, delete
import sqlite3
# connect() creates the file if it does not exist
# Use ":memory:" for a temporary in-memory database
conn = sqlite3.connect(":memory:")
cursor = conn.cursor()
# CREATE TABLE
cursor.execute("""
CREATE TABLE users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
email TEXT UNIQUE NOT NULL,
age INTEGER
)
""")
# INSERT — use ? placeholders, never f-strings (SQL injection risk)
users = [
("Alice", "alice@example.com", 30),
("Bob", "bob@example.com", 25),
("Charlie", "charlie@example.com", 35),
]
cursor.executemany(
"INSERT INTO users (name, email, age) VALUES (?, ?, ?)", users
)
conn.commit() # persist changes
# SELECT
cursor.execute("SELECT id, name, age FROM users WHERE age > ?", (26,))
rows = cursor.fetchall() # list of tuples
for row in rows:
print(row)
# UPDATE
cursor.execute("UPDATE users SET age = ? WHERE name = ?", (31, "Alice"))
conn.commit()
# DELETE
cursor.execute("DELETE FROM users WHERE name = ?", ("Bob",))
conn.commit()
# Verify final state
cursor.execute("SELECT * FROM users")
print("\nFinal table:")
for row in cursor.fetchall():
print(row)
conn.close()(3, 'Charlie', 35)
Final table:
(1, 'Alice', 'alice@example.com', 31)
(3, 'Charlie', 'charlie@example.com', 35)
- Always use
?placeholders — never string-format values into SQL queries. Doing so opens a SQL injection vulnerability. conn.commit()saves changes — without it, inserts/updates/deletes are not persistedcursor.fetchall()returns all rows as a list of tuples;cursor.fetchone()returns a single rowcursor.executemany()inserts multiple rows efficiently in one call
Using sqlite3 as a Context Manager
SQLite connections work as context managers. The with conn: block automatically commits on success and rolls back on exception — cleaner and safer than manual commit/rollback.
# Context manager — automatic commit and rollback
import sqlite3
conn = sqlite3.connect(":memory:")
# Create table
conn.execute("""
CREATE TABLE products (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
price REAL NOT NULL
)
""")
# with conn: automatically commits on success, rolls back on exception
with conn:
conn.execute("INSERT INTO products (name, price) VALUES (?, ?)", ("Notebook", 4.99))
conn.execute("INSERT INTO products (name, price) VALUES (?, ?)", ("Pen", 1.50))
conn.execute("INSERT INTO products (name, price) VALUES (?, ?)", ("Desk", 89.99))
# Use Row factory for dict-like access
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
cursor.execute("SELECT * FROM products ORDER BY price")
for row in cursor.fetchall():
print(f"{row['name']:10} ${row['price']:.2f}")
conn.close()Notebook $4.99
Desk $89.99
conn.row_factory = sqlite3.Rowlets you access columns by name (row["name"]) instead of index (row[0])- The
with conn:context manager handles transactions — it does not close the connection - Call
conn.close()explicitly when done, or usewith sqlite3.connect(...) as conn:for both
SQLAlchemy — The Pythonic Database Toolkit
SQLAlchemy is the standard Python database library for production applications. It provides two levels: the Core (close to raw SQL but with Python objects) and the ORM (maps Python classes to tables, abstracts SQL almost entirely). Both use the same connection engine and work with SQLite, PostgreSQL, MySQL, and more — swap the connection string and the code stays the same.
# SQLAlchemy — installation and engine setup
# pip install sqlalchemy
from sqlalchemy import create_engine, text
# SQLite in-memory — swap URL for PostgreSQL, MySQL etc. without changing any other code
# PostgreSQL: "postgresql://user:password@localhost/dbname"
# MySQL: "mysql+pymysql://user:password@localhost/dbname"
engine = create_engine("sqlite:///:memory:", echo=False)
# execute raw SQL through SQLAlchemy
with engine.connect() as conn:
conn.execute(text("""
CREATE TABLE employees (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
department TEXT,
salary REAL
)
"""))
conn.execute(text("""
INSERT INTO employees (name, department, salary) VALUES
('Alice', 'Engineering', 95000),
('Bob', 'Marketing', 72000),
('Charlie', 'Engineering', 88000),
('Diana', 'HR', 65000)
"""))
conn.commit()
# Query with parameters
with engine.connect() as conn:
result = conn.execute(
text("SELECT name, salary FROM employees WHERE department = :dept ORDER BY salary DESC"),
{"dept": "Engineering"}
)
for row in result:
print(f"{row.name}: ${row.salary:,.0f}")Charlie: $88,000
create_engine(url)sets up the connection — it does not open a connection immediately- Use
:param_nameplaceholders with a dict in SQLAlchemy (not?like bare sqlite3) engine.connect()as a context manager auto-closes the connection- Changing the database is just changing the URL string — all query code remains identical
SQLAlchemy ORM — Classes as Tables
The ORM maps Python classes to database tables. You define your schema once as Python classes and interact with the database entirely through objects — no SQL strings required for most operations.
Real-world use: Django and Flask-SQLAlchemy both use ORM patterns. Every model in a web app — User, Order, Product — is an ORM class.
# SQLAlchemy ORM — define models and query with Python objects
from sqlalchemy import create_engine, Column, Integer, String, Float
from sqlalchemy.orm import DeclarativeBase, Session
engine = create_engine("sqlite:///:memory:")
# Define the base class all models inherit from
class Base(DeclarativeBase):
pass
# Define a model — maps to the "products" table
class Product(Base):
__tablename__ = "products"
id = Column(Integer, primary_key=True, autoincrement=True)
name = Column(String, nullable=False)
category = Column(String)
price = Column(Float, nullable=False)
def __repr__(self):
return f"Product({self.name!r}, ${self.price:.2f})"
# Create all tables
Base.metadata.create_all(engine)
# Insert rows using Python objects
with Session(engine) as session:
session.add_all([
Product(name="Laptop", category="Electronics", price=999.99),
Product(name="Notebook", category="Stationery", price=4.99),
Product(name="Monitor", category="Electronics", price=349.99),
Product(name="Pen", category="Stationery", price=1.50),
])
session.commit()
# Query — filter, order, limit
with Session(engine) as session:
# All electronics sorted by price
electronics = (
session.query(Product)
.filter(Product.category == "Electronics")
.order_by(Product.price.desc())
.all()
)
for p in electronics:
print(p)
# Count all products
total = session.query(Product).count()
print(f"\nTotal products: {total}")Product('Monitor', $349.99)
Total products: 4
- Each model class inherits from
Baseand uses__tablename__to name the table session.add(obj)stages one object;session.add_all([...])stages manysession.commit()persists all staged changes;session.rollback()cancels themsession.query(Model).filter(...).order_by(...).all()is the standard query pattern
ORM — Update and Delete
# ORM — updating and deleting records
with Session(engine) as session:
# Update — fetch, modify, commit
laptop = session.query(Product).filter(Product.name == "Laptop").first()
if laptop:
laptop.price = 899.99
session.commit()
print("Updated:", laptop)
# Delete — fetch and delete
pen = session.query(Product).filter(Product.name == "Pen").first()
if pen:
session.delete(pen)
session.commit()
print("Deleted pen")
# Verify
remaining = session.query(Product).all()
print("Remaining products:", remaining)Deleted pen
Remaining products: [Product('Laptop', $899.99), Product('Notebook', $4.99), Product('Monitor', $349.99)]
Summary Table
| Tool | Best For | Key Usage |
|---|---|---|
sqlite3 |
Scripts, prototypes, local storage | conn.execute(sql, params) |
| SQLAlchemy Core | Flexible SQL with Python, multi-DB | conn.execute(text(sql), params) |
| SQLAlchemy ORM | Applications, web apps, complex models | session.query(Model).filter(...) |
conn.row_factory |
Named column access in sqlite3 | sqlite3.Row |
? / :name |
Safe parameter binding | Always use — never f-string SQL values |
Practice Questions
Practice 1. Why should you never use f-strings or string formatting to insert values into SQL queries?
Practice 2. What does conn.commit() do in sqlite3?
Practice 3. What does setting conn.row_factory = sqlite3.Row enable?
Practice 4. In SQLAlchemy ORM, what method stages multiple new objects for insertion in one call?
Practice 5. What is the main advantage of using SQLAlchemy over bare sqlite3 for production applications?
Quiz
Quiz 1. What connection string would you use for an in-memory SQLite database?
Quiz 2. What happens inside a with conn: block in sqlite3 if an exception is raised?
Quiz 3. In SQLAlchemy ORM, what class attribute tells SQLAlchemy which table a model maps to?
Quiz 4. What is the placeholder syntax for named parameters in SQLAlchemy's text() queries?
Quiz 5. Which SQLAlchemy method creates all tables defined by ORM models in the database?
Next up — CSV & XML: reading, writing, and parsing structured data files in Python.