Chapter 16 - SQLite Databases (Python)
Here's a concise walkthrough of the main ideas in Chapter 16, each with a small example.
Why databases and why SQLite
- Databases (using SQL) make complex queries easy, compared with scanning text files/spreadsheets (e.g., "all cats between 3–5 kg born before Oct 2023").
- SQLite is a lightweight, embedded SQL database included with Python (
sqlite3module), storing the whole database in a single file. - It's ideal when you'd otherwise use JSON/CSV/spreadsheets but need fast, structured querying.
Spreadsheets vs databases
- A table ≈ a spreadsheet; rows = records, columns = fields.
- SQLite automatically adds a
rowidprimary key column (unique integer) that doesn't change when rows are deleted/reordered, unlike spreadsheet row numbers. - Spreadsheets often serve as formatted forms (fixed size, lots of styling) which don't map well to database tables; databases hold raw, structured data.
SQLite vs other SQL databases
Key differences:
- Single file, easy to move/backup.
- Runs in-process, no server or network.
- Very small footprint, can live entirely in memory.
- Loose typing by default (but strict mode exists).
- No user/permission system (no
GRANT/REVOKE). - Public domain, no licensing restrictions.
Limitation: not good for massive concurrent writes (e.g. high-traffic websites), but fine for large data sets and many reads.
Creating databases and tables
Connecting to a database
Use sqlite3.connect() to open or create a database file, getting a Connection object.
import sqlite3
conn = sqlite3.connect('example.db', isolation_level=None)
# isolation_level=None → autocommit mode, no explicit conn.commit() needed
Call conn.close() when done (also happens automatically at program exit).
Creating a table
Use CREATE TABLE IF NOT EXISTS and conn.execute(sql_string).
import sqlite3
conn = sqlite3.connect('example.db', isolation_level=None)
conn.execute('''
CREATE TABLE IF NOT EXISTS cats (
name TEXT NOT NULL,
birthdate TEXT,
fur TEXT,
weight_kg REAL
) STRICT
''')
IF NOT EXISTSavoids errors if the table already exists.- Convention: uppercase SQL keywords, lowercase table/column names with underscores.
Data types and STRICT mode
SQLite types:
NULL(likeNone)INTEGER(likeint)REAL(likefloat)TEXT(likestr)BLOB(likebytes)
By default, SQLite is flexible: "42" in an INTEGER column becomes 42, "Hello" is stored even if column is INTEGER (type affinity).
STRICT table option (SQLite ≥ 3.37, Python ≥ 3.11) turns on strict mode:
- Every column must have a type.
- Wrong-type inserts raise
sqlite3.IntegrityError. - Auto-casting still happens when possible (
"42"→ 42), but"Hello"in anINTEGERcolumn fails.
No built-in Boolean type: use INTEGER 0/1.
No date/datetime type: store as TEXT in forms like YYYY-MM-DD, YYYY-MM-DD HH:MM:SS.
Check SQLite version:
import sqlite3
print(sqlite3.sqlite_version) # '3.xx.xx'
name TEXT NOT NULL disallows NULL in name.
Listing tables and columns
List tables via sqlite_schema:
import sqlite3
conn = sqlite3.connect('example.db', isolation_level=None)
tables = conn.execute(
'SELECT name FROM sqlite_schema WHERE type="table"'
).fetchall()
print(tables) # [('cats',)]
Get table schema:
cols = conn.execute('PRAGMA TABLE_INFO(cats)').fetchall()
print(cols)
# e.g. (1, 'birthdate', 'TEXT', 0, None, 0)
Each tuple: (position, name, type, notnull_flag, default_value, is_primary_key).
Do not modify sqlite_schema manually; you can corrupt the database.
CRUD database operations
CRUD = Create, Read, Update, Delete → INSERT, SELECT, UPDATE, DELETE.
Example statements:
INSERT INTO cats VALUES ("Zophie", "2021-01-24", "black", 5.6)SELECT rowid, * FROM cats ORDER BY furUPDATE cats SET fur = "gray tabby" WHERE rowid = 1DELETE FROM cats WHERE rowid = 1
Most applications are just UIs around CRUD operations: posting = INSERT, editing = UPDATE, deleting = DELETE, browsing = SELECT.
Inserting data
Simple insert:
import sqlite3
conn = sqlite3.connect('example.db', isolation_level=None)
conn.execute('''
INSERT INTO cats VALUES ("Zophie", "2021-01-24", "black", 5.6)
''')
Transactions and ACID
Each INSERT (or any write) is a transaction; SQLite ensures ACID:
- Atomic: all or nothing.
- Consistent: respects constraints (
NOT NULL, etc.). - Isolated: doesn't interfere with other transactions.
- Durable: results survive crashes/power loss (tested).
Avoiding SQL injection with placeholders
When inserting user-provided data, use ? placeholders instead of string formatting.
Bad:
cat_name = 'Zophie'
# f-string version can be vulnerable if values come from untrusted input
conn.execute(
f'INSERT INTO cats VALUES ("{cat_name}", "2021-01-24", "black", 5.6)'
)
Good:
cat_name = 'Zophie'
cat_bday = '2021-01-24'
fur_color = 'black'
cat_weight = 5.6
conn.execute(
'INSERT INTO cats VALUES (?, ?, ?, ?)',
[cat_name, cat_bday, fur_color, cat_weight],
)
execute safely escapes values into the query.
Reading data (SELECT)
Basic read:
import sqlite3
conn = sqlite3.connect('example.db', isolation_level=None)
rows = conn.execute('SELECT * FROM cats').fetchall()
print(rows) # [('Zophie', '2021-01-24', 'black', 5.6)]
execute()returns a Cursor; call.fetchall()to get rows as a list of tuples.SELECT column1, column2 ... FROM tablechooses columns;*means all columns (not includingrowid; you must request it explicitly).
Example selecting specific columns:
rows = conn.execute('SELECT rowid, name FROM cats').fetchall()
print(rows) # [(1, 'Zophie')]
Looping over results
You can iterate directly over conn.execute(...) without calling .fetchall().
import sqlite3
conn = sqlite3.connect('sweigartcats.db', isolation_level=None)
for row in conn.execute('SELECT * FROM cats'):
print('Row data:', row)
print(row[0], 'is one of my favorite cats.')
Filtering (WHERE), matching (LIKE / GLOB), sorting (ORDER BY), limiting (LIMIT)
WHERE clause
Filter rows:
rows = conn.execute(
'SELECT * FROM cats WHERE fur = "black"'
).fetchall()
Operators similar to Python: =, !=, <, >, <=, >=, AND, OR, NOT (note = for equality).
Example with combined conditions:
import pprint
matching_cats = conn.execute(
'SELECT * FROM cats WHERE fur = "black" OR birthdate >= "2024-01-01"'
).fetchall()
pprint.pprint(matching_cats)
Because dates are stored as strings YYYY-MM-DD, comparisons like >= "2024-01-01" work lexicographically as date comparisons.
LIKE with % wildcards (case-insensitive)
%= any substring.name LIKE "%y"– names ending withy.name LIKE "Ja%"– names starting withJa.name LIKE "%ob%"– names containingobanywhere.
rows = conn.execute(
'SELECT rowid, name FROM cats WHERE name LIKE "%y"'
).fetchall()
LIKE is case-insensitive (%ob% matches ob, OB, etc.).
GLOB with * wildcards (case-sensitive)
GLOB uses shell-style patterns and is case-sensitive.
rows = conn.execute(
'SELECT rowid, name FROM cats WHERE name GLOB "*m*"'
).fetchall()
Matches only lowercase m.
ORDER BY
Sort results in the database:
import pprint
rows = conn.execute(
'SELECT * FROM cats ORDER BY fur'
).fetchall()
pprint.pprint(rows)
Multiple sort keys and directions:
rows = conn.execute(
'SELECT * FROM cats ORDER BY fur ASC, birthdate DESC'
).fetchall()
If a WHERE exists, ORDER BY must come after it.
LIMIT
Return only a subset efficiently:
rows = conn.execute(
'SELECT * FROM cats LIMIT 3'
).fetchall()
With filter and sort:
rows = conn.execute(
'SELECT * FROM cats WHERE fur="orange" '
'ORDER BY birthdate LIMIT 4'
).fetchall()
LIMIT must come after WHERE and ORDER BY.
Indexes for faster reads
Create indexes to speed up WHERE queries on specific columns (trade-off: slightly slower writes, more disk space).
conn.execute('CREATE INDEX idx_name ON cats (name)')
conn.execute('CREATE INDEX idx_birthdate ON cats (birthdate)')
List indexes for a table:
idxs = conn.execute(
'SELECT name FROM sqlite_schema WHERE type="index" AND tbl_name="cats"'
).fetchall()
print(idxs) # [('idx_name',), ('idx_birthdate',)]
Drop an index:
conn.execute('DROP INDEX idx_name')
For small tables, indexes usually don't matter; for large/slow queries, they can help.
Updating data (UPDATE)
Change existing rows using UPDATE ... SET ... WHERE ... and placeholders for values.
Example: change Zophie's fur color:
conn.execute(
'UPDATE cats SET fur = ? WHERE name = ?',
['gray tabby', 'Zophie'],
)
Without a WHERE, all rows would be updated, so always include a condition unless that's intentional.
Deleting data (DELETE)
Remove rows using DELETE FROM ... WHERE ....
Example: delete one cat by rowid:
conn.execute('DELETE FROM cats WHERE rowid = 1')
Delete all cats with black fur:
conn.execute('DELETE FROM cats WHERE fur = "black"')
Again, omit WHERE only if you truly intend to delete all rows in the table.
Overall idea of the chapter
Chapter 16 introduces SQLite as a lightweight, file-based database built into Python. The key concepts are: connecting with sqlite3.connect(), creating tables with typed columns and STRICT mode, CRUD operations (INSERT/SELECT/UPDATE/DELETE), filtering with WHERE/LIKE/GLOB, sorting with ORDER BY, limiting with LIMIT, indexes for performance, and always using ? placeholders to prevent SQL injection.