NANDHOO.

Databases with SQLite & SQLAlchemy

Databases with SQLite & SQLAlchemy


Most real-world applications need to store data permanently. This chapter covers working with SQLite (a lightweight file-based database) directly, and then using SQLAlchemy — the most popular Python ORM — to interact with databases using Python objects.


Why This Chapter Matters


Whether you are building a web app, a CLI tool, or a data pipeline, databases are the backbone of persistent storage. Knowing both raw SQL and an ORM gives you flexibility to choose the right tool for each situation.


SQL Basics Refresher


SQL (Structured Query Language) is used to interact with relational databases.


-- Create a table
CREATE TABLE users (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT NOT NULL,
    email TEXT UNIQUE NOT NULL,
    score REAL DEFAULT 0
);

-- Insert data INSERT INTO users (name, email) VALUES ('Asha', 'asha@example.com');


-- Query data SELECT * FROM users WHERE score > 80 ORDER BY score DESC;


-- Update UPDATE users SET score = 95 WHERE name = 'Asha';


-- Delete DELETE FROM users WHERE id = 1;


SQLite with Python's Built-in sqlite3


SQLite is a lightweight, file-based database that requires no server. Python ships with sqlite3 built-in.


import sqlite3

Connect (creates file if it doesn't exist)

conn = sqlite3.connect("students.db") cursor = conn.cursor()


Create table

cursor.execute(""" CREATE TABLE IF NOT EXISTS students ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, score REAL NOT NULL ) """)


Insert data

cursor.execute("INSERT INTO students (name, score) VALUES (?, ?)", ("Asha", 95)) cursor.execute("INSERT INTO students (name, score) VALUES (?, ?)", ("Leo", 88))


Commit the transaction

conn.commit()


Query

cursor.execute("SELECT * FROM students ORDER BY score DESC") rows = cursor.fetchall()


for row in rows: print(row)


Close connection

conn.close()


Using with for Safe Connections


with sqlite3.connect("students.db") as conn:
    cursor = conn.cursor()
    cursor.execute("SELECT * FROM students")
    for row in cursor.fetchall():
        print(row)
# connection is committed and closed automatically

Using Row Factory for Dict-like Access


with sqlite3.connect("students.db") as conn:
    conn.row_factory = sqlite3.Row   # rows behave like dicts
    cursor = conn.cursor()
    cursor.execute("SELECT * FROM students")
    for row in cursor.fetchall():
        print(row["name"], row["score"])

Parameterized Queries (Prevent SQL Injection)


Always use ? placeholders, never format strings directly:


# BAD — vulnerable to SQL injection
cursor.execute(f"SELECT * FROM users WHERE name = '{name}'")

GOOD — parameterized

cursor.execute("SELECT * FROM users WHERE name = ?", (name,))


SQLAlchemy — The Python ORM


SQLAlchemy lets you interact with databases using Python objects instead of raw SQL. It supports SQLite, PostgreSQL, MySQL, and more.


Installing


pip install sqlalchemy

Defining Models


from sqlalchemy import create_engine, Column, Integer, String, Float
from sqlalchemy.orm import DeclarativeBase, Session

Create engine (SQLite for this example)

engine = create_engine("sqlite:///school.db", echo=True)


class Base(DeclarativeBase): pass


class Student(Base): tablename = "students"


id = Column(Integer, primary_key=True, autoincrement=True)
name = Column(String(100), nullable=False)
email = Column(String(200), unique=True, nullable=False)
score = Column(Float, default=0.0)

def __repr__(self):
    return f"Student(name={self.name!r}, score={self.score})"

Create all tables

Base.metadata.create_all(engine)


Creating Records


with Session(engine) as session:
    asha = Student(name="Asha", email="asha@example.com", score=95)
    leo = Student(name="Leo", email="leo@example.com", score=88)
    
    session.add(asha)
    session.add(leo)
    session.add_all([
        Student(name="Mina", email="mina@example.com", score=92)
    ])
    session.commit()

Querying Records


with Session(engine) as session:
    # Get all
    students = session.query(Student).all()

# Filter
top_students = session.query(Student).filter(Student.score >= 90).all()

# Order
sorted_students = session.query(Student).order_by(Student.score.desc()).all()

# Single record
asha = session.query(Student).filter_by(name="Asha").first()
by_id = session.get(Student, 1)

for student in top_students:
    print(student)

Updating Records


with Session(engine) as session:
    student = session.query(Student).filter_by(name="Leo").first()
    student.score = 95
    session.commit()

Deleting Records


with Session(engine) as session:
    student = session.query(Student).filter_by(name="Mina").first()
    session.delete(student)
    session.commit()

Relationships (Foreign Keys)


from sqlalchemy import ForeignKey
from sqlalchemy.orm import relationship

class Course(Base): tablename = "courses" id = Column(Integer, primary_key=True) title = Column(String, nullable=False) students = relationship("Enrollment", back_populates="course")


class Enrollment(Base): tablename = "enrollments" id = Column(Integer, primary_key=True) student_id = Column(Integer, ForeignKey("students.id")) course_id = Column(Integer, ForeignKey("courses.id")) student = relationship("Student") course = relationship("Course", back_populates="students")


Environment Variables for Database URLs


Never hard-code database credentials. Use environment variables:


import os
from dotenv import load_dotenv
from sqlalchemy import create_engine

load_dotenv()


DATABASE_URL = os.environ.get("DATABASE_URL", "sqlite:///default.db") engine = create_engine(DATABASE_URL)


.env file:


DATABASE_URL=postgresql://user:password@localhost:5432/mydb

Common Database Operations Pattern


def get_student(student_id: int):
    with Session(engine) as session:
        return session.get(Student, student_id)

def create_student(name: str, email: str, score: float = 0.0): with Session(engine) as session: student = Student(name=name, email=email, score=score) session.add(student) session.commit() session.refresh(student) return student


def update_score(student_id: int, new_score: float): with Session(engine) as session: student = session.get(Student, student_id) if not student: raise ValueError(f"Student {student_id} not found") student.score = new_score session.commit()


Common Mistakes


  • forgetting to commit() after INSERT/UPDATE/DELETE
  • using session outside of a with block (risk of unclosed sessions)
  • constructing SQL strings with f-strings (SQL injection risk)
  • not using foreign keys and missing cascading behavior
  • running migrations manually without a migration tool (use Alembic for SQLAlchemy)

Mini Exercises


  1. Create a SQLite database with sqlite3 that stores books (title, author, year).
  2. Insert 5 books and query all books published after 2000.
  3. Using SQLAlchemy, define a Product model with name, price, and category.
  4. Write functions to create, read, update, and delete products (CRUD).
  5. Add a Category model and set up a one-to-many relationship with Product.

Review Questions


  1. What is the difference between SQLite and PostgreSQL?
  2. Why must you always use parameterized queries instead of string formatting?
  3. What is an ORM and what problem does it solve?
  4. What does session.commit() do in SQLAlchemy?
  5. How do you define a relationship between two models in SQLAlchemy?

Reference Checklist


  • I can create and query SQLite databases using sqlite3
  • I use parameterized queries to prevent SQL injection
  • I can define SQLAlchemy models with columns and relationships
  • I can perform CRUD operations with SQLAlchemy Sessions
  • I know how to load database URLs from environment variables