Skip to main content

Parameterized Queries: The Law

intermediate15 min readLesson 89 of 169

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.

Now practice

Injection-Proof DrillsEvery query parameterized; identifiers whitelisted.2 challenges ยท ยท ~30 min