Python sqlite3 Parameterized Queries

Passing Python values into a SQLite query means keeping those values separate from the SQL instructions. Python’s sqlite3 does this with parameterized queries, which use placeholders to mark value positions and pass the values separately when the statement runs.

I’ll show how to bind values with placeholders and how to build an IN clause for a list.

TL;DR: Bind Values with Placeholders

Pass values as the second argument to execute(), using one ? marker per value or named markers with a dictionary.

  • Use placeholders for values, never string formatting.
  • Use a tuple or other sequence with qmark markers.
  • Use a dictionary with named markers.
  • Build an IN list with one marker per item, then allowlist identifiers separately.
import sqlite3

con = sqlite3.connect(":memory:")
con.execute("CREATE TABLE users (email TEXT)")
con.execute("INSERT INTO users VALUES (?)", ("ada@example.com",))
email = "ada@example.com"
row = con.execute("SELECT email FROM users WHERE email = ?", (email,)).fetchone()
print(row)
con.close()
("ada@example.com",)

What Are Parameterized Queries in Python?

A parameterized query is an SQL statement whose values are supplied separately when it runs. In Python’s sqlite3, execute() passes those values to SQLite for binding, rather than formatting them into the SQL string.

SQLite compiles the statement structure with placeholders, then binds each supplied value to its marker. The marker stands for a value such as text, a number, or NULL. It cannot stand for SQL grammar such as a table name or the word DESC.

The separation matters because SQL has syntax and values. The SQL text tells SQLite which table to read and which comparison to make. Bound parameters supply the values used by that comparison, keeping a value from changing the statement’s structure.

In the query shown below, the question mark occupies the value position in the WHERE condition. A value containing a quote or SQL keyword remains data when SQLite binds it. Python’s sqlite3 documentation recommends placeholders over string formatting for this reason.

This distinction blocks a value such as ‘ OR 1=1 — from becoming part of the WHERE clause. It remains one text value, while the query structure stays unchanged. The same binding approach applies to SELECT, INSERT, UPDATE, and DELETE statements.

How to Use sqlite3 Parameters Step by Step

Use a connection, create a small table, then pass each query value as a separate argument to execute(). I use an in-memory database here so the example has no filesystem side effects.

Step 1: Bind one value with a qmark placeholder

Put a question mark where the value belongs, then pass a one-item tuple as the second argument. The comma makes (email,) a tuple rather than a parenthesized string.

import sqlite3

con = sqlite3.connect(":memory:")
con.execute("CREATE TABLE users (id INTEGER, email TEXT)")
con.execute("INSERT INTO users VALUES (?, ?)", (1, "ada@example.com"))
email = "ada@example.com"
rows = con.execute(
    "SELECT id FROM users WHERE email = ?", (email,)
).fetchall()
print(rows)
con.close()
[(1,)]

The SQL string contains the query shape. The tuple holds the value that SQLite binds at the question mark.

  • Each qmark needs one supplied value.
  • With two qmarks, supply two values in the same order.
  • A missing or extra value raises a binding error, so keep marker count and sequence length aligned.

Step 2: Use named placeholders for longer queries

Named markers make the mapping between a query and its values visible. Use a dictionary whose keys match the marker names without the colon.

import sqlite3

con = sqlite3.connect(":memory:")
con.execute("CREATE TABLE users (id INTEGER, email TEXT)")
con.execute("INSERT INTO users VALUES (:id, :email)",
            {"id": 1, "email": "ada@example.com"})
row = con.execute(
    "SELECT id FROM users WHERE email = :email",
    {"email": "ada@example.com"},
).fetchone()
print(row)
con.close()
(1,)

Named placeholders suit statements with several values because each value is labelled. The dictionary form also avoids accidentally swapping positional arguments.

Python sqlite3 supports qmark style and named style. For named placeholders, pair the marker names in SQL with keys in a mapping. Use a consistent style within each statement so each input’s destination remains clear.

Step 3: Reuse a parameterized statement for multiple rows

Use executemany() when each row uses the same SQL structure. Each sequence supplies one set of values for the statement.

import sqlite3

con = sqlite3.connect(":memory:")
con.execute("CREATE TABLE users (id INTEGER, email TEXT)")
users = [(1, "ada@example.com"), (2, "grace@example.com")]
con.executemany("INSERT INTO users VALUES (?, ?)", users)
print(con.execute("SELECT COUNT(*) FROM users").fetchone())
con.close()
(2,)

The SQL is reused for both rows, while the values change for each execution. For transaction details, see the site’s Python SQLite guide.

Step 4: Keep user input out of the SQL string

An f-string inserts text before SQLite parses the query. That lets input alter SQL syntax, so use placeholders for every data value instead.

import sqlite3

con = sqlite3.connect(":memory:")
con.execute("CREATE TABLE users (id INTEGER, email TEXT)")
con.execute("INSERT INTO users VALUES (?, ?)", (1, "ada@example.com"))
email = "ada@example.com' OR 1=1 --"

# Unsafe: input becomes part of the SQL text
query = f"SELECT id FROM users WHERE email = '{email}'"
print(con.execute(query).fetchall())

# Safe: SQLite receives the value separately
query = "SELECT id FROM users WHERE email = ?"
rows = con.execute(query, (email,)).fetchall()
print(rows)
con.close()
[(1,)]
[]

Parameter binding treats quote characters in email as ordinary value content. It does not validate whether the email is legitimate, so application-level validation still belongs in the calling code.

Binding also does not authorize a user to access a row. Keep input validation and permission checks in the application even when every SQL value is bound. Do not add quotes around a placeholder because the marker already represents a complete value.

Edge Cases: Lists and SQL Identifiers

These cases matter when a query is assembled from a variable number of inputs or a user-selected sort field. Keep values bound and keep SQL structure selected from trusted program logic.

Placeholders bind individual values, but they do not expand a collection or replace SQL identifiers. Keep the statement structure controlled by your code, and generate only the required number of value markers.

  • For one value, use one placeholder.
  • For a list, generate one placeholder per item.
  • For a table or column name, choose from a fixed allowlist.

Build an IN list with one marker per value

SQLite expects one placeholder for each item in an IN clause. Create the markers from the list length and pass the values as a tuple.

import sqlite3

con = sqlite3.connect(":memory:")
con.execute("CREATE TABLE users (id INTEGER, email TEXT)")
con.executemany("INSERT INTO users VALUES (?, ?)", [(1, "ada@example.com"), (2, "grace@example.com"), (3, "linus@example.com")])
user_ids = [1, 2, 3]
marks = ", ".join("?" for _ in user_ids)
sql = f"SELECT id FROM users WHERE id IN ({marks})"
rows = con.execute(sql, tuple(user_ids)).fetchall()
print(rows)
con.close()
[(1,), (2,), (3,)]

The f-string here inserts only a fixed comma-separated run of question marks generated by the program, not user-provided values. Handle an empty list separately because an empty IN expression is not portable SQL. Choose the behavior explicitly, such as returning no rows without querying.

This approach works for a list whose length is known when building the statement. The list elements remain bound parameters, so strings containing quotes cannot add SQL operators. For an empty list, decide the result before constructing SQL, such as returning an empty result for an empty filter.

Choose dynamic identifiers from an allowlist

A placeholder cannot stand for a column or table name because those names define SQL syntax. Select an identifier from a fixed mapping, then bind the comparison value normally.

import sqlite3

con = sqlite3.connect(":memory:")
con.execute("CREATE TABLE users (id INTEGER, email TEXT)")
con.executemany("INSERT INTO users VALUES (?, ?)", [(1, "ada@example.com"), (2, "grace@example.com")])
requested_sort = "email"
columns = {"email": "email", "id": "id"}
column = columns.get(requested_sort)
if column is None:
    raise ValueError("unsupported sort field")

sql = f"SELECT id, email FROM users ORDER BY {column}"
rows = con.execute(sql).fetchall()
print(rows)
con.close()
[(1, 'ada@example.com'), (2, 'grace@example.com')]

Only strings supplied by the allowlist enter the SQL structure. For a WHERE value or LIMIT value, use a placeholder rather than adding it to the statement.

An allowlist maps an external choice to a fixed SQL identifier written by the application. It is safer than escaping arbitrary identifier text because SQLite parameters bind values, not grammar. Keep the external key separate from the SQL string and reject keys that have no mapping.

Sorting direction needs the same treatment. Choose between fixed ASC and DESC fragments in application logic. Bind all ordinary values with placeholders.

InputHow to include it
Search term or idBind with a placeholder
List of idsGenerate one marker per item and bind the sequence
Column or table nameChoose a fixed identifier from an allowlist
Sort directionSelect a fixed ASC or DESC fragment

Related SQL topics on this site include the SQLite SELECT statement and the SQLite WHERE clause.

When debugging a query, inspect the SQL template and the parameter collection as separate pieces. Confirm that every placeholder has a matching value, then check whether any dynamic table, column, or sort fragment comes from an explicit allowlist. The Python driver passes data through sqlite3 binding, so logging the template is safer than constructing a second interpolated query for debugging.

Named placeholders can make logs and reviews clearer because each value has a label. Positional placeholders are concise for a short statement with a stable order. Either style depends on matching parameters to markers, and both keep values outside the SQL syntax.

Bindings are also useful for values that look like numbers, dates, or punctuation. Let sqlite3 adapt Python values instead of adding quotes and conversion rules yourself. If an application accepts a value from a form or API, passing it in the parameter sequence keeps its contents out of the SQL grammar.

Do not use execute() parameters as a substitute for validating input. A bound value can still be outside the allowed business range or fail an application rule.

Binding answers a narrower question: whether the value can alter SQL structure. Validate meaning and permissions separately.

Conclusion: Keep SQL Structure Separate from Values

Parameterized queries keep a statement’s SQL syntax separate from the values supplied at runtime. Use qmark or named placeholders with execute(), generate one marker per item for IN lists, and choose dynamic identifiers only from an allowlist.

For the underlying database behavior, see Python’s sqlite3 documentation and the SQLite documentation on binding values.

FAQ

Can I use a placeholder for a table name in sqlite3?

No. Placeholders represent values, not SQL identifiers. Choose a table or column name from a fixed allowlist and bind ordinary data values with placeholders.

How do I pass a list to an IN clause?

Create one question-mark placeholder for each item in the list, join the markers with commas, and pass the values as a tuple. Handle an empty list separately.

Should I use qmark or named placeholders?

Both are supported by Python sqlite3. Qmark style pairs question marks with a sequence, while named style pairs markers such as :email with a dictionary. Choose the form that keeps the value mapping clear.