Chapters
Python114 chapters

Beyond the basicsChapter 99 of 114

SQLite

A real database in a single file, with no server, built into Python.

No server, no install

sqlite3 is in the standard library, and a whole database is one file. That makes it the right default for a desktop app, a cache, or anything where JSON has stopped being enough.

Python
import sqlite3

connection = sqlite3.connect("people.db")
cursor = connection.cursor()

cursor.execute("CREATE TABLE IF NOT EXISTS people (name TEXT, born INTEGER)")
cursor.execute("INSERT INTO people VALUES ('Ada', 1815)")
connection.commit()

cursor.execute("SELECT * FROM people")
print(cursor.fetchall())

connection.close()

Output

[('Ada', 1815)]

Pass ":memory:" instead of a filename for a database that lives only as long as the program — ideal for tests.

Never build SQL with string formatting

Python
import sqlite3

connection = sqlite3.connect(":memory:")
cursor = connection.cursor()
cursor.execute("CREATE TABLE people (name TEXT, born INTEGER)")

cursor.execute("INSERT INTO people VALUES (?, ?)", ("Ada", 1815))
cursor.executemany("INSERT INTO people VALUES (?, ?)", [("Grace", 1906), ("Kat", 1918)])
connection.commit()

cursor.execute("SELECT name FROM people WHERE born > ?", (1900,))
print([row[0] for row in cursor.fetchall()])

Output

['Grace', 'Kat']

The ? placeholders are the whole point. The value is sent separately from the statement, so it can never be read as SQL.

Note the trailing comma in (1900,). Parameters go in a tuple, and a one-item tuple needs it.

Fetching

Python
import sqlite3

connection = sqlite3.connect(":memory:")
connection.execute("CREATE TABLE people (name TEXT, born INTEGER)")
connection.executemany("INSERT INTO people VALUES (?, ?)", [("Ada", 1815), ("Grace", 1906)])

cursor = connection.execute("SELECT * FROM people ORDER BY born")
print(cursor.fetchone())
print(cursor.fetchall())

for row in connection.execute("SELECT name FROM people"):
    print(row[0])

Output

('Ada', 1815)
[('Grace', 1906)]
Ada
Grace

fetchone took the first row, so fetchall returned only the rest. A cursor is an iterator, and looping over it is the memory-safe way to read a large result.

Rows by name

Tuples get unreadable fast. sqlite3.Row gives you access by column name:

Python
import sqlite3

connection = sqlite3.connect(":memory:")
connection.row_factory = sqlite3.Row
connection.execute("CREATE TABLE people (name TEXT, born INTEGER)")
connection.execute("INSERT INTO people VALUES (?, ?)", ("Ada", 1815))

row = connection.execute("SELECT * FROM people").fetchone()
print(row["name"], row["born"])
print(dict(row))

Output

Ada 1815
{'name': 'Ada', 'born': 1815}

Now adding a column cannot break code that reads by position.

Transactions

Changes are not saved until you commit, which is what makes "all or nothing" possible:

Python
import sqlite3

connection = sqlite3.connect(":memory:")
connection.execute("CREATE TABLE people (name TEXT)")

try:
    with connection:
        connection.execute("INSERT INTO people VALUES ('Ada')")
        connection.execute("INSERT INTO people VALUES ('Grace')")
        raise ValueError("something went wrong")
except ValueError:
    print("rolled back")

print(connection.execute("SELECT COUNT(*) FROM people").fetchone()[0])

Output

rolled back
0

Using the connection as a context manager commits on success and rolls back on an exception. Neither insert survived, which is exactly right — a half-finished change is usually worse than none.

Schema

Python
import sqlite3

connection = sqlite3.connect(":memory:")
connection.execute("""
    CREATE TABLE people (
        id      INTEGER PRIMARY KEY,
        name    TEXT NOT NULL UNIQUE,
        born    INTEGER
    )
""")

connection.execute("INSERT INTO people (name, born) VALUES ('Ada', 1815)")

try:
    connection.execute("INSERT INTO people (name, born) VALUES ('Ada', 1900)")
except sqlite3.IntegrityError:
    print("UNIQUE stopped the duplicate")

print(connection.execute("SELECT id, name FROM people").fetchall())

Output

UNIQUE stopped the duplicate
[(1, 'Ada')]

INTEGER PRIMARY KEY auto-numbers for you. Constraints in the schema are worth far more than checks in your code, because the database enforces them however the data arrives.

Closing

Python
import sqlite3

with sqlite3.connect(":memory:") as connection:
    connection.execute("CREATE TABLE t (n INTEGER)")
    connection.execute("INSERT INTO t VALUES (1)")
    print(connection.execute("SELECT COUNT(*) FROM t").fetchone()[0])

connection.close()
print("closed")

Output

1
closed

Test yourself

2 questions

Why use ? placeholders instead of an f-string in a query?

Show the answer

The value is sent separately, so it can never be read as SQL — A name of ' OR '1'='1 returns every row. There is no situation where the f-string is worth it.

What does using the connection as a context manager do?

Show the answer

Commits on success and rolls back on an exception — It commits the transaction but does not close the connection. Call close() yourself.

Next chapter

Packaging

Turn a folder of scripts into something installable.