Database IO with sqlite3
Store and query structured data from Python using the built-in SQLite library.
- Open a database connection and use it as a context manager
- Execute parameterised queries to avoid SQL injection
- Bulk-insert rows with executemany
- Configure row_factory for dict-like row access
SQLite is a full relational database engine in a single file — no server, no
configuration, no separate install. Python ships sqlite3 in the standard
library, making it the go-to choice for local data storage, small analytical
pipelines, and prototyping database schemas before moving to Postgres or MySQL.
The API follows the Python DB-API 2.0 standard, so the patterns you learn here
transfer directly.
Connecting and the connection as a context manager
sqlite3.connect takes a file path (or ":memory:" for a transient in-memory
database):
import sqlite3
# on-disk database — created if it does not exist
conn = sqlite3.connect("data.db")
# in-memory — great for tests and throwaway pipelines
conn = sqlite3.connect(":memory:")Use the connection as a context manager for automatic transaction handling:
the with block commits on success and rolls back on any exception.
with sqlite3.connect("data.db") as conn:
conn.execute("CREATE TABLE IF NOT EXISTS items (name TEXT, qty INTEGER)")
conn.execute("INSERT INTO items VALUES (?, ?)", ("widget", 5))
# conn is still open here; the with block committed the transaction
conn.close()The context manager controls the transaction, not the connection lifetime.
Call conn.close() explicitly when you're done, or open the connection inside
a try/finally. A future contextlib.closing wrapper makes this fully
automatic.
Parameterised queries — always use ? placeholders
Never build SQL strings with f-strings or % formatting. A rogue value like
"'; DROP TABLE items; --" becomes an injection attack. Use ? placeholders
instead — the driver handles quoting and escaping:
# WRONG — vulnerable to SQL injection
name = input("name: ")
conn.execute(f"INSERT INTO items VALUES ('{name}', 1)") # never do this
# RIGHT — parameterised, safe
conn.execute("INSERT INTO items VALUES (?, ?)", (name, 1))The same rule applies to every query that takes user input or external data —
SELECT, UPDATE, DELETE, all of them.
Fetching results
A cursor (returned by execute) holds the result set. Three methods cover
all use cases:
cursor = conn.execute("SELECT name, qty FROM items WHERE qty > ?", (0,))
row = cursor.fetchone() # next row as a tuple, or None
rows = cursor.fetchall() # all remaining rows as a list of tuples
chunk = cursor.fetchmany(50) # next 50 rows — good for large result setsFor one-liner reads, conn.execute(...).fetchall() skips naming the cursor.
executemany for bulk inserts
Loading many rows one execute at a time means many round-trips through the
driver. executemany batches them into a single operation:
records = [("Alice", 92.5), ("Bob", 78.0), ("Carol", 88.3)]
conn.execute("CREATE TABLE scores (name TEXT, score REAL)")
conn.executemany("INSERT INTO scores VALUES (?, ?)", records)
conn.commit()Write load_products(records) that: (1) opens an in-memory database, (2) creates a table "products" with columns name (TEXT) and price (REAL), (3) inserts all records using executemany with ? placeholders, and (4) returns all rows as a list of tuples ordered by price ascending.
load_products([("Apple", 0.5), ("Banana", 0.3)]) → [('Banana', 0.3), ('Apple', 0.5)]row_factory for dict-like rows
By default, rows come back as plain tuples. Set row_factory to sqlite3.Row
and each row behaves like a dict keyed by column name — much easier to work with
in real code:
conn.row_factory = sqlite3.Row
row = conn.execute("SELECT name, score FROM scores LIMIT 1").fetchone()
print(row["name"]) # 'Alice' — column access by name
print(row["score"]) # 92.5
print(dict(row)) # {'name': 'Alice', 'score': 92.5}Set row_factory before running any queries. It applies to every cursor
created from that connection.
sqlite3.Row objects are not regular dicts — they don't support item
assignment and they expire when the cursor is closed. If you need to hold rows
beyond the query's lifetime, convert with dict(row) or [dict(r) for r in rows].
Where to go next
You can now persist and query structured data. The next lesson covers the bitwise and boolean tools that help you validate, filter, and flag that data efficiently.