Python SQLite String Formatting: %s vs {0}.format() vs ? – Key Differences and Best Practices Explained

SQLite is a lightweight, file-based database engine widely used in Python applications for its simplicity, portability, and zero-configuration setup. When working with SQLite in Python, a critical task is constructing SQL queries dynamically—for example, inserting user input into a query or fetching data based on variable parameters. However, how you format these dynamic queries can have profound implications for security, reliability, and maintainability.

In Python, three common approaches for string formatting in SQLite queries are:

  • Old-style %s formatting (e.g., "... %s ..." % var),
  • New-style str.format() (e.g., "... {0} ...".format(var)),
  • Parameterized queries with ? (e.g., cursor.execute("... ? ...", (var,))).

While the first two methods are familiar to many Python developers for general string manipulation, they pose severe risks when used for SQL queries. The third method—parameterized queries—stands out as the safest and most reliable option.

This blog will demystify these three approaches, compare their strengths and weaknesses, and outline best practices to ensure your SQLite queries are secure, efficient, and error-free.

Table of Contents#

  1. Understanding the Basics of SQLite in Python
  2. Old-Style String Formatting with %s
  3. New-Style String Formatting with str.format()
  4. Parameterized Queries with ?
  5. Key Differences: %s vs {0}.format() vs ?
  6. Best Practices for SQLite String Formatting
  7. Common Pitfalls to Avoid
  8. Conclusion
  9. References

Understanding the Basics of SQLite in Python#

Before diving into string formatting, let’s recap how SQLite works in Python. Python’s built-in sqlite3 module provides a simple interface to interact with SQLite databases. A typical workflow involves:

  1. Connecting to a database (or creating one if it doesn’t exist).
  2. Creating a cursor to execute SQL commands.
  3. Executing queries (e.g., CREATE TABLE, INSERT, SELECT).
  4. Committing changes (for write operations like INSERT or UPDATE).
  5. Closing the connection.

Here’s a minimal example:

import sqlite3  
 
# Connect to database (creates "mydb.db" if it doesn't exist)  
conn = sqlite3.connect("mydb.db")  
cursor = conn.cursor()  
 
# Create a table  
cursor.execute("CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT)")  
 
# Insert data (we'll focus on this step for formatting)  
name = "Alice"  
# We'll replace this with different formatting methods below  
cursor.execute("INSERT INTO users (name) VALUES ('Alice')")  
 
# Commit and close  
conn.commit()  
conn.close()  

The critical line here is cursor.execute(...), where dynamic values (like name) need to be inserted safely. Let’s examine the three formatting approaches for this task.

Old-Style String Formatting with %s#

Old-style string formatting uses the % operator, with %s as a placeholder for strings (other placeholders like %d for integers exist, but %s is most common). For example:

Example: Inserting Data with %s#

name = "Alice"  
query = "INSERT INTO users (name) VALUES ('%s')" % name  
cursor.execute(query)  

At first glance, this seems straightforward: %s is replaced with the value of name, resulting in the query:
INSERT INTO users (name) VALUES ('Alice').

Problems with %s Formatting#

1. SQL Injection Vulnerability#

The biggest risk is SQL injection, where malicious users can manipulate input to alter the query’s logic. For example, if name is set to:

name = "Robert'); DROP TABLE users;--"  

The formatted query becomes:

INSERT INTO users (name) VALUES ('Robert'); DROP TABLE users;--')  

This would execute two commands: insert "Robert" and then drop the users table! This is catastrophic.

2. Manual Handling of Quotes and Types#

%s requires you to manually add quotes around string values (e.g., ('%s')). Forgetting quotes or mishandling non-string types (e.g., integers) leads to syntax errors. For example:

age = 30  
query = "INSERT INTO users (age) VALUES (%s)" % age  # Missing quotes (but age is an integer, so this works... for now)  
# But if age is a string (e.g., "30"), you need quotes: VALUES ('%s')  

This inconsistency is error-prone.

3. Special Character Issues#

If name contains special characters like single quotes (e.g., O'Neil), the query breaks:

name = "O'Neil"  
query = "INSERT INTO users (name) VALUES ('%s')" % name  
# Result: INSERT INTO users (name) VALUES ('O'Neil')  ❌ Syntax error (unclosed quote)  

New-Style String Formatting with {0}.format()#

Python 3 introduced str.format(), a more flexible alternative to %s. It supports positional placeholders ({0}, {1}), named placeholders ({name}), and formatting options (e.g., {:.2f} for floats).

Example: Inserting Data with str.format()#

name = "Alice"  
query = "INSERT INTO users (name) VALUES ('{0}')".format(name)  
cursor.execute(query)  

This produces the same query as %s, but with more flexibility (e.g., "{name}".format(name=name) for readability).

Problems with str.format()#

Despite its flexibility, str.format() shares the same flaws as %s:

1. Still Vulnerable to SQL Injection#

Using the same malicious input as before:

name = "Robert'); DROP TABLE users;--"  
query = "INSERT INTO users (name) VALUES ('{0}')".format(name)  
# Result: INSERT INTO users (name) VALUES ('Robert'); DROP TABLE users;--')  ❌ Same SQL injection risk!  

2. Manual Quote and Type Management#

Like %s, you must manually handle quotes and type conversion. For O'Neil:

name = "O'Neil"  
query = "INSERT INTO users (name) VALUES ('{0}')".format(name)  
# Result: INSERT INTO users (name) VALUES ('O'Neil')  ❌ Syntax error  

3. No Improvement in Safety#

str.format() is more powerful for general string formatting (e.g., "{:.2f}".format(3.1415)), but it does not address the core issue of separating query structure from data.

Parameterized Queries with ?#

The only safe and recommended method for dynamic SQLite queries in Python is parameterized queries using ? as placeholders. With this approach, you separate the query template from the data, passing parameters as a second argument to cursor.execute().

Example: Inserting Data with ?#

name = "Alice"  
query = "INSERT INTO users (name) VALUES (?)"  # ? is the placeholder  
cursor.execute(query, (name,))  # Data passed as a tuple  

How It Works#

  • Query Template: The query string contains ? placeholders for dynamic values (no quotes needed around ?).
  • Parameters: Values are passed as a second argument to cursor.execute() (as a tuple, even for single values: (name,)).
  • Database Driver Handles Escaping: The sqlite3 driver automatically escapes special characters, adds quotes for strings, and converts types (e.g., integers, dates) correctly.

Why ? is Safe#

1. Prevents SQL Injection#

Malicious input is treated as literal data, not executable SQL. For example:

name = "Robert'); DROP TABLE users;--"  
cursor.execute("INSERT INTO users (name) VALUES (?)", (name,))  

The database interprets this as inserting the literal string Robert'); DROP TABLE users;-- into the name column—no additional commands are executed.

2. Automatic Type Handling#

No need for manual quotes or type conversion. The driver handles:

  • Strings (adds quotes and escapes special characters like ').
  • Integers/floats (no quotes).
  • None (converted to NULL).

Example with O'Neil:

name = "O'Neil"  
cursor.execute("INSERT INTO users (name) VALUES (?)", (name,))  
# Result: INSERT INTO users (name) VALUES ('O''Neil')  ✅ Escaped single quote  

3. Cleaner, More Maintainable Code#

Separating query structure from data makes code easier to read and debug.

Key Differences: %s vs {0}.format() vs ?#

To summarize, here’s a comparison of the three methods:

Feature%s Formattingstr.format()Parameterized ?
SQL Injection RiskHigh (vulnerable)High (vulnerable)None (safe)
Type HandlingManual (quotes, conversion)Manual (quotes, conversion)Automatic (driver handles)
Special CharactersBreaks queries (e.g., O'Neil)Breaks queries (e.g., O'Neil)Escapes automatically
FlexibilityLimited (positional only)High (positional, named, formatting)Limited (only ? placeholders)
Recommended ForNever for SQL queriesNever for SQL queriesAlways for SQL queries

Best Practices for SQLite String Formatting#

1. Always Use Parameterized Queries (?)#

This is non-negotiable for security and reliability. Reserve %s and str.format() for general string tasks (e.g., logging), not SQL.

2. Use Named Placeholders (Optional)#

sqlite3 also supports named placeholders with :name (instead of ?), which can improve readability for queries with many parameters:

cursor.execute(  
    "INSERT INTO users (name, age) VALUES (:name, :age)",  
    {"name": "Alice", "age": 30}  # Dictionary instead of tuple  
)  

3. Avoid Dynamic Table/Column Names#

Parameterized queries work for values, but not for identifiers like table or column names (e.g., SELECT * FROM ? is invalid). If you must use dynamic identifiers (e.g., user-specified table names), sanitize inputs rigorously:

allowed_tables = {"users", "products"}  # Whitelist allowed tables  
table_name = "users"  # User input (validate against allowed_tables)  
 
if table_name not in allowed_tables:  
    raise ValueError("Invalid table name")  
 
query = f"SELECT * FROM {table_name}"  # Only safe if sanitized!  
cursor.execute(query)  

4. Use Context Managers for Connections#

Ensure connections/cursors are closed properly with with statements:

with sqlite3.connect("mydb.db") as conn:  
    with conn.cursor() as cursor:  
        cursor.execute("INSERT INTO users (name) VALUES (?)", (name,))  
    conn.commit()  # Commits when the outer `with` exits  

Common Pitfalls to Avoid#

  • Forgetting the Comma in Single-Parameter Tuples:
    cursor.execute("SELECT * FROM users WHERE name = ?", (name)) ❌ (missing comma creates a string, not a tuple).
    Use (name,) ✅.

  • Mixing Placeholder Styles:
    sqlite3 uses ? or :name, but other databases (e.g., PostgreSQL) use %s. Stick to ? for SQLite.

  • Overlooking NULL Values:
    None is automatically converted to NULL, so use cursor.execute("INSERT INTO users (name) VALUES (?)", (None,)) for NULL entries.

Conclusion#

When working with SQLite in Python, string formatting for queries is not just about readability—it’s about security and reliability. While %s and str.format() are familiar for general string tasks, they expose your application to SQL injection and syntax errors. Parameterized queries with ? are the only safe choice, as they separate query structure from data, letting the database driver handle escaping and type conversion.

By adopting ? (or :name) placeholders, validating dynamic identifiers, and using context managers, you’ll write secure, maintainable SQLite code.

References#