SQLite is a serverless database that lives in a single file. Python's sqlite3 module lets you use it without installing anything.

Basic Connection

import sqlite3
 
# Connect (creates file if it doesn't exist)
conn = sqlite3.connect('mydb.db')
cursor = conn.cursor()
 
# In-memory database (gone when connection closes)
conn = sqlite3.connect(':memory:')

Always use context managers:

with sqlite3.connect('mydb.db') as conn:
    cursor = conn.cursor()
    # Work with database
# Auto-commits on success, rolls back on exception

Creating Tables

cursor.execute('''
    CREATE TABLE IF NOT EXISTS users (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        name TEXT NOT NULL,
        email TEXT UNIQUE,
        created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
    )
''')
conn.commit()

Parameterized Queries

Never use string formatting with SQL. Use parameters:

# BAD: SQL injection risk
cursor.execute(f"SELECT * FROM users WHERE name = '{name}'")
 
# GOOD: parameterized (? placeholder)
cursor.execute("SELECT * FROM users WHERE name = ?", (name,))
 
# GOOD: named parameters
cursor.execute(
    "SELECT * FROM users WHERE name = :name AND email = :email",
    {"name": name, "email": email}
)

CRUD Operations

Insert

# Single row
cursor.execute(
    "INSERT INTO users (name, email) VALUES (?, ?)",
    ("Alice", "alice@example.com")
)
 
# Multiple rows
users = [
    ("Bob", "bob@example.com"),
    ("Charlie", "charlie@example.com"),
]
cursor.executemany(
    "INSERT INTO users (name, email) VALUES (?, ?)",
    users
)
 
# Get inserted ID
cursor.execute("INSERT INTO users (name) VALUES (?)", ("Dave",))
print(cursor.lastrowid)
 
conn.commit()

Select

# Fetch all
cursor.execute("SELECT * FROM users")
rows = cursor.fetchall()
 
# Fetch one
cursor.execute("SELECT * FROM users WHERE id = ?", (1,))
row = cursor.fetchone()
 
# Iterate directly
for row in cursor.execute("SELECT name, email FROM users"):
    print(row[0], row[1])

Update

cursor.execute(
    "UPDATE users SET email = ? WHERE id = ?",
    ("new@example.com", 1)
)
print(f"Updated {cursor.rowcount} rows")
conn.commit()

Delete

cursor.execute("DELETE FROM users WHERE id = ?", (1,))
conn.commit()

Row Factories

Get dict-like rows instead of tuples:

conn.row_factory = sqlite3.Row
 
cursor = conn.cursor()
cursor.execute("SELECT * FROM users WHERE id = ?", (1,))
row = cursor.fetchone()
 
# Access by name
print(row["name"])
print(row["email"])
 
# Still works as tuple
print(row[0], row[1])

Transactions

conn = sqlite3.connect('mydb.db')
 
try:
    cursor = conn.cursor()
    cursor.execute("UPDATE accounts SET balance = balance - 100 WHERE id = 1")
    cursor.execute("UPDATE accounts SET balance = balance + 100 WHERE id = 2")
    conn.commit()
except Exception:
    conn.rollback()
    raise

Or use with:

with sqlite3.connect('mydb.db') as conn:
    cursor = conn.cursor()
    cursor.execute("UPDATE accounts SET balance = balance - 100 WHERE id = 1")
    cursor.execute("UPDATE accounts SET balance = balance + 100 WHERE id = 2")
# Auto-commits if no exception

Common Patterns

Check if Table Exists

cursor.execute("""
    SELECT name FROM sqlite_master 
    WHERE type='table' AND name=?
""", ('users',))
exists = cursor.fetchone() is not None

Upsert (Insert or Update)

cursor.execute("""
    INSERT INTO users (id, name, email) VALUES (?, ?, ?)
    ON CONFLICT(id) DO UPDATE SET 
        name = excluded.name,
        email = excluded.email
""", (1, "Alice", "alice@example.com"))

Bulk Insert with Transaction

users = [(f"user_{i}", f"user{i}@example.com") for i in range(10000)]
 
conn.execute("BEGIN")
cursor.executemany(
    "INSERT INTO users (name, email) VALUES (?, ?)",
    users
)
conn.commit()

Type Adapters

SQLite has limited types. Adapt Python types:

import json
from datetime import datetime
 
# Register adapter for datetime
sqlite3.register_adapter(datetime, lambda dt: dt.isoformat())
sqlite3.register_converter("TIMESTAMP", lambda b: datetime.fromisoformat(b.decode()))
 
# Register adapter for dict/list as JSON
sqlite3.register_adapter(dict, lambda d: json.dumps(d))
sqlite3.register_adapter(list, lambda l: json.dumps(l))
sqlite3.register_converter("JSON", lambda b: json.loads(b.decode()))
 
# Enable converters
conn = sqlite3.connect('mydb.db', detect_types=sqlite3.PARSE_DECLTYPES)

Performance Tips

# Use WAL mode for concurrent reads
conn.execute("PRAGMA journal_mode=WAL")
 
# Sync less often (faster, slightly less safe)
conn.execute("PRAGMA synchronous=NORMAL")
 
# Use transactions for bulk operations
conn.execute("BEGIN")
# ... many inserts ...
conn.commit()

Quick Reference

OperationMethod
Connectsqlite3.connect('file.db')
Executecursor.execute(sql, params)
Batch executecursor.executemany(sql, list)
Fetch onecursor.fetchone()
Fetch allcursor.fetchall()
Commitconn.commit()
Rollbackconn.rollback()
Last insert IDcursor.lastrowid
Rows affectedcursor.rowcount

SQLite is perfect for single-user apps, prototypes, and embedded systems. When you outgrow it, the SQL knowledge transfers to PostgreSQL or MySQL.

React to this post: