Day 21: Introduction to Databases — SQLite with Python
🐍 Day 21: Introduction to Databases — SQLite with Python
1. Learning Objectives
By the end of Day 21, you will be able to:
- Understand what a relational database is and why it's used over files
- Create and connect to a SQLite database file using Python
- Execute SQL commands from Python: CREATE TABLE, INSERT, SELECT, UPDATE, DELETE
- Use parameterized queries to prevent SQL injection
- Work with cursors and transactions
- Apply best practices for database connections (using context managers)
- Build a simple data‑driven application with persistent storage
2. Concept Explanation
2.1 Why Databases? — Beyond Simple Files
So far we stored data in JSON, CSV, or plain text files. Those work for small data, but as your app grows, you need:
| Need | File Solution | Database Solution |
|---|---|---|
| Fast search | Load everything, loop manually | SQL queries with indexes |
| Multiple users | File locks, corruption risk | ACID transactions |
| Complex relationships | Nested JSON, manual linking | JOINs, foreign keys |
| Data integrity | Manual validation | Constraints (NOT NULL, UNIQUE) |
A database is a structured collection of data managed by a DBMS (Database Management System). Today we'll use SQLite — the world's most deployed database, built right into Python.
2.2 What is SQLite?
- Lightweight: Serverless — it's just a single file (
.dbor.sqlite). - Zero configuration: No setup, no daemons, no passwords.
- Built‑in: The
sqlite3module is part of Python's standard library. - Ideal for: desktop apps, mobile apps, embedded systems, prototyping, testing.
SQLite uses full SQL (Structured Query Language) — the same language used by PostgreSQL, MySQL, etc. The skills you learn today transfer directly to those systems.
2.3 Core SQL Concepts (Quick Reference)
A relational database stores data in tables (like spreadsheets). Each table has columns (fields) and rows (records).
-- A simple users table
CREATE TABLE users (
id INTEGER PRIMARY KEY, -- auto-incrementing unique ID
name TEXT NOT NULL, -- required text
email TEXT UNIQUE, -- must be unique
age INTEGER
);
Common SQL statements:
| Statement | Purpose |
|---|---|
CREATE TABLE | Define a new table |
INSERT INTO | Add rows |
SELECT | Query rows |
UPDATE | Modify existing rows |
DELETE | Remove rows |
DROP TABLE | Delete entire table |
2.4 Connecting to SQLite with Python
import sqlite3
# Connect (creates the file if it doesn't exist)
conn = sqlite3.connect("my_database.db")
# Create a cursor to execute SQL
cursor = conn.cursor()
# Execute a statement
cursor.execute("CREATE TABLE IF NOT EXISTS people (name TEXT, age INTEGER)")
# Commit changes (save)
conn.commit()
# Close connection
conn.close()
2.5 CRUD Operations in Python
Create (Insert):
import sqlite3
conn = sqlite3.connect("example.db")
cursor = conn.cursor()
# Insert a single row using parameterized query (safe!)
cursor.execute("INSERT INTO people (name, age) VALUES (?, ?)", ("Alice", 28))
# Insert multiple rows
users = [("Bob", 35), ("Charlie", 22), ("Diana", 30)]
cursor.executemany("INSERT INTO people (name, age) VALUES (?, ?)", users)
conn.commit()
conn.close()
⚠️ Never use string formatting for SQL!
f"INSERT INTO ... VALUES ('{name}', {age})"invites SQL injection attacks. Always use?placeholders (or:namenamed placeholders).
Read (Select):
conn = sqlite3.connect("example.db")
cursor = conn.cursor()
cursor.execute("SELECT * FROM people")
all_rows = cursor.fetchall()
for row in all_rows:
print(row) # ('Alice', 28), ('Bob', 35), ...
# Fetch one row
cursor.execute("SELECT name, age FROM people WHERE age > ?", (25,))
row = cursor.fetchone()
while row:
print(row)
row = cursor.fetchone()
conn.close()
Update:
conn = sqlite3.connect("example.db")
cursor = conn.cursor()
cursor.execute("UPDATE people SET age = ? WHERE name = ?", (29, "Alice"))
conn.commit()
conn.close()
Delete:
conn = sqlite3.connect("example.db")
cursor = conn.cursor()
cursor.execute("DELETE FROM people WHERE name = ?", ("Charlie",))
conn.commit()
conn.close()
2.6 Using the Connection as a Context Manager
Python's sqlite3 connections support the with statement. They auto‑commit on success and rollback on exception.
with sqlite3.connect("example.db") as conn:
cursor = conn.cursor()
cursor.execute("INSERT INTO people (name, age) VALUES (?, ?)", ("Eve", 42))
# If an exception occurs, the INSERT is automatically rolled back
# Connection is automatically committed if no exception occurred
This is the recommended pattern — it prevents partial writes and reduces bugs.
2.7 Practical Example — A Simple Task Database
import sqlite3
def init_db(db_name="tasks.db"):
with sqlite3.connect(db_name) as conn:
conn.execute("""
CREATE TABLE IF NOT EXISTS tasks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
completed INTEGER DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
""")
def add_task(title, db_name="tasks.db"):
with sqlite3.connect(db_name) as conn:
conn.execute("INSERT INTO tasks (title) VALUES (?)", (title,))
def list_tasks(db_name="tasks.db"):
with sqlite3.connect(db_name) as conn:
cursor = conn.execute("SELECT id, title, completed FROM tasks")
for row in cursor.fetchall():
status = "✓" if row[2] else "○"
print(f"{row[0]}. [{status}] {row[1]}")
def complete_task(task_id, db_name="tasks.db"):
with sqlite3.connect(db_name) as conn:
conn.execute("UPDATE tasks SET completed = 1 WHERE id = ?", (task_id,))
# Usage
init_db()
add_task("Learn SQLite")
add_task("Build a database app")
list_tasks()
complete_task(1)
list_tasks()
3. Code Examples
Example 1: Bookstore Database
import sqlite3
DB = "bookstore.db"
def setup():
with sqlite3.connect(DB) as conn:
conn.execute("""
CREATE TABLE IF NOT EXISTS books (
isbn TEXT PRIMARY KEY,
title TEXT NOT NULL,
author TEXT NOT NULL,
price REAL
)
""")
def add_book(isbn, title, author, price):
with sqlite3.connect(DB) as conn:
conn.execute(
"INSERT OR IGNORE INTO books VALUES (?, ?, ?, ?)",
(isbn, title, author, price)
)
def search_by_author(author):
with sqlite3.connect(DB) as conn:
cursor = conn.execute(
"SELECT title, price FROM books WHERE author LIKE ?",
(f"%{author}%",) # % for partial match
)
return cursor.fetchall()
# Test
setup()
add_book("978-0132350884", "Clean Code", "Robert C. Martin", 42.99)
add_book("978-0201616224", "The Pragmatic Programmer", "David Thomas", 49.99)
print(search_by_author("Martin"))
Example 2: Using Named Parameters
with sqlite3.connect("example.db") as conn:
conn.execute(
"INSERT INTO people (name, age) VALUES (:name, :age)",
{"name": "Frank", "age": 45}
)
Example 3: Handling None / NULL Values
# Inserting NULL
conn.execute("INSERT INTO people (name, age) VALUES (?, ?)", ("Grace", None))
# Selecting and handling None
cursor.execute("SELECT name, age FROM people WHERE age IS NULL")
for row in cursor.fetchall():
print(row) # ('Grace', None)
4. Hands-On Exercises
Exercise 1: Create a Movie Database
Create a table movies with columns: id (auto-increment), title, year, rating. Insert 5 movies. Query and print all movies.
Exercise 2: Filter Queries
Expand Exercise 1: write a function get_movies_by_year(year) that returns all movies from a given year. Test with different years.
Exercise 3: Update and Delete
Add functions to update a movie's rating given its id, and to delete a movie by id. Test both.
Exercise 4: User Registration System
Create a users table with id, username (unique), email, password_hash. Write functions to register a user and to check if a username already exists. Use parameterized queries.
Exercise 5: Transaction with Rollback
Write a script that starts a transaction, inserts two rows, then intentionally raises an exception between them. Observe that neither row is inserted because the transaction is rolled back. Use a with block.
5. Applied Challenge Task 🏗️
Personal Library Manager with SQLite
Build a complete console application to manage your personal book library, backed by SQLite.
Requirements:
Database schema:
- Table
books:id(INTEGER PRIMARY KEY),title(TEXT NOT NULL),author(TEXT),isbn(TEXT UNIQUE),status(TEXT DEFAULT 'unread'),rating(INTEGER). - The
statuscan be"unread","reading", or"finished".
- Table
Menu options:
=== LIBRARY MANAGER === [1] Add Book [2] List All Books [3] Update Status [4] Rate Book (1-5) [5] Search by Author [6] Delete Book [7] Stats (total, read, unread) [8] QuitFeatures:
- Add book with validation: if ISBN already exists, show error.
- Search by author using partial matching (
LIKE). - Stats show total books, number read, unread, currently reading.
Technical requirements:
- Use
with sqlite3.connect(...)for all operations. - Use parameterized queries exclusively.
- Handle
sqlite3.IntegrityErrorfor duplicate ISBNs.
- Use
Stretch goals:
- Export the library to a JSON file.
- Add a
date_addedcolumn with default current timestamp. - Provide an option to sort by title, author, or rating.
- Write a simple command-line argument parser (using
argparse) for non‑interactive queries.
6. Brief Review Summary
| Concept | Key Points |
|---|---|
| SQLite | Serverless, file‑based, built‑in sqlite3 module |
| Table | A collection of rows with defined columns |
| CRUD | Create (INSERT), Read (SELECT), Update (UPDATE), Delete (DELETE) |
| Parameterized queries | Use ? or :name — never string interpolation |
with connection | Auto‑commit on success, rollback on exception |
| Cursor | Executes SQL and fetches results (fetchall, fetchone) |
| Transactions | Group operations; all succeed or all fail |
7. Preview of Next Topic — Day 22
Tomorrow we shift to ensuring our code actually works:
- Software Testing — why we test and types of tests
- Writing unit tests with
pytest - Assertions, fixtures, and test discovery
- Testing functions, classes, and database operations
- Test‑Driven Development (TDD) introduction
🎯 Your Action Items for Day 21:
- ✅ Complete all 5 exercises
- ✅ Build the Personal Library Manager with SQLite
- ✅ Experiment with raw SQL in the
sqlite3shell (sqlite3.exeorsqlite3command) - ✅ Try breaking your queries — see what errors SQLite raises
Comments
Post a Comment
Leave us your comments here...