Parameterized Queries: The Law
Never build SQL with string formatting โ placeholders separate code from data.
The single most important database habit, stated as a law: SQL and data
travel separately. Placeholders (? in sqlite3) hand values to the driver,
which binds them safely:
# GOOD: the value is data, never executable SQL
conn.execute("SELECT * FROM users WHERE name = ?", (user_input,))
# NEVER: string formatting builds executable SQL from input
conn.execute(f"SELECT * FROM users WHERE name = '{user_input}'")
Why the f-string version is catastrophic: input ' OR '1'='1 turns the query
into WHERE name = '' OR '1'='1' โ matching every user. That is SQL
injection, and it has topped vulnerability lists for two decades. A parameter
binds ' OR '1'='1 as a literal string to compare, defusing it completely.
What parameters cannot do
Placeholders only work where a value belongs: after =, IN (...),
VALUES. Identifiers (table names, column names) cannot be parameterized โ
if those must be dynamic, validate against a whitelist:
ALLOWED_SORTS = {"title": "title", "created": "id"}
column = ALLOWED_SORTS.get(user_choice) # None โ reject
if column is None:
raise ValueError("invalid sort column")
executemany for batches
conn.executemany("INSERT INTO tasks (title) VALUES (?)", [("a",), ("b",)])
One round trip for many rows โ and one more place the driver handles the escaping for you.