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.
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
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
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
Gracefetchone 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:
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:
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
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
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 questionsWhy 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.
Packaging
Turn a folder of scripts into something installable.