Code of the Day
AdvancedData Engineering

Database IO with sqlite3

Store and query structured data from Python using the built-in SQLite library.

PythonAdvanced10 min read
By the end of this lesson you will be able to:
  • 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 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 sets

For 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()
Create table and bulk loadPython

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.

Finished reading? Mark it complete to track your progress.

On this page