If you’ve been writing SQL in Python, you already know the controversy: positional parameters (?) or named parameters (%(title)s)? Some builders swear by the conciseness of positional. Others choose the readability of named. With , you now not want to decide on – we assist .
We’ve added twin parameter model assist to mssql-python, enabling each qmark and pyformat parameter kinds in Python functions that work together with SQL Server and Azure SQL. This characteristic is particularly helpful if you’re constructing advanced queries, dynamically assembling filters, or migrating present code that already makes use of named parameters with different DBAPI drivers.
Attempt it right here
You possibly can set up driver utilizing
Calling all Python + SQL builders! We invite the neighborhood to check out mssql-python and assist us form the way forward for high-performance .!
What Are Parameter Types?
The DB-API 2.0 specification (PEP 249) defines a number of methods to move parameters to SQL queries. The 2 hottest are:
- qmark – Positional ? placeholders with a tuple/checklist of values.
- pyformat – Named %(title)s placeholders with a dictionary of values.
# qmark model cursor.execute("SELECT * FROM customers WHERE id = ? AND standing = ?", (42, "lively")) # pyformat model cursor.execute("SELECT * FROM customers WHERE id = %(id)s AND standing = %(standing)s", {"id": 42, "standing": "lively"})
Enterprise Requirement
Beforehand, mssql-python solely supported qmark. It really works fantastic for easy queries, however as parameters multiply, monitoring their order turns into error-prone:
# Which ? corresponds to which worth?
cursor.execute(
"UPDATE customers SET title=?, e mail=?, age=? WHERE id=? AND standing=?",
(title, e mail, age, user_id, standing)
)
Combine up the order and it’s straightforward to introduce delicate, arduous to spot bugs.
Why Named Parameters?
- Self-documenting queries – No extra guessing which ? maps to what:
qmark — 6 parameters, which is which?
cursor.execute( """INSERT INTO staff (first_name, last_name, e mail, division, wage, hire_date) VALUES (?, ?, ?, ?, ?, ?)""", ("Jane", "Doe", "jane.doe@firm.com", "Engineering", 95000, "2025-03-01") )
pyformat — each worth is labeled
cursor.execute( """INSERT INTO staff (first_name, last_name, e mail, division, wage, hire_date) VALUES (%(first_name)s, %(last_name)s, %(e mail)s, %(dept)s, %(wage)s, %(hire_date)s)""", {"first_name": "Jane", "last_name": "Doe", "e mail": "jane.doe@firm.com", "dept": "Engineering", "wage": 95000, "hire_date": "2025-03-01"} )
- Parameter reuse – Use the identical worth a number of instances with out repeating it:
Audit log: report who made the change and when
cursor.execute( """UPDATE orders SET standing = %(new_status)s, modified_by = %(person)s, approved_by = %(person)s, modified_at = %(now)s, approved_at = %(now)s WHERE order_id = %(order_id)s""", {"new_status": "authorized", "person": "admin@firm.com", "now": datetime.now(), "order_id": 5042} )
3 distinctive values, used 5 instances — no duplication wanted
- Dynamic question constructing – Add filters with out monitoring parameter positions:
def search_orders(buyer=None, standing=None, min_total=None, date_from=None):
query_parts = ["SELECT * FROM orders WHERE 1=1"]
params = {}
if buyer:
query_parts.append("AND customer_id = %(buyer)s")
params["customer"] = buyer
if standing:
query_parts.append("AND standing = %(standing)s")
params["status"] = standing
if min_total isn't None:
query_parts.append("AND whole >= %(min_total)s")
params["min_total"] = min_total
if date_from:
query_parts.append("AND order_date >= %(date_from)s")
params["date_from"] = date_from
query_parts.append("ORDER BY order_date DESC")
cursor.execute(" ".be part of(query_parts), params)
return cursor.fetchall()
# Callers use solely the filters they want
recent_big_orders = search_orders(min_total=500, date_from="2025-01-01")
pending_for_alice = search_orders(buyer=42, standing="pending")
- Dictionary Reuse Throughout Queries
The identical parameter dictionary can drive a number of queries:
report_params = {"area": "West", "yr": 2025, "standing": "lively"}
# Abstract rely
cursor.execute(
"""SELECT COUNT(*) FROM prospects
WHERE area = %(area)s AND standing = %(standing)s""",
report_params
)
whole = cursor.fetchone()[0]
# Income breakdown
cursor.execute(
"""SELECT division, SUM(income)
FROM gross sales
WHERE area = %(area)s AND fiscal_year = %(yr)s
GROUP BY division
ORDER BY SUM(income) DESC""",
report_params
)
breakdown = cursor.fetchall()
# High performers
cursor.execute(
"""SELECT title, income
FROM sales_reps
WHERE area = %(area)s AND fiscal_year = %(yr)s AND standing = %(standing)s
ORDER BY income DESC""",
report_params
)
top_reps = cursor.fetchall()
# Identical dict, three completely different queries — change the filters as soon as, all queries replace
The Resolution: Automated Detection
mssql-python now detects which model you’re utilizing based mostly on the parameter sort:
- tuple/checklist → qmark (?)
- dict → pyformat (%(title)s)
No configuration wanted. Current qmark code requires zero modifications.
from mssql_python import join
# qmark - works precisely as earlier than
cursor.execute("SELECT * FROM customers WHERE id = ?", (42,))
# pyformat - simply move a dict!
cursor.execute("SELECT * FROM customers WHERE id = %(id)s", {"id": 42})
How It Works
If you move a dict to execute(), the motive force:
- Scans the SQL for %(title)s placeholders (context-aware – skips string literals, feedback, and bracketed identifiers).
- Validates that each placeholder has an identical key within the dict.
- Builds a positional tuple in placeholder order (duplicating values for reused parameters).
- Replaces every %(title)s with ? and sends the rewritten question to ODBC.
Consumer Code ODBC Layer
───────── ──────────
cursor.execute( SQLBindParameter(1, "lively")
"WHERE standing = %(standing)s SQLBindParameter(2, "USA")
AND nation = %(nation)s", → SQLExecute(
{"standing": "lively", "WHERE standing = ?
"nation": "USA"} AND nation = ?"
) )
The ODBC layer all the time works with positional ? placeholders. The pyformat conversion is only a developer-facing comfort with zero overhead to database communication.
Clear Error Messages
Mismatched kinds or lacking parameters produce actionable errors – not cryptic database exceptions:
cursor.execute("WHERE id = %(id)s AND title = %(title)s", {"id": 42})
# KeyError: Lacking required parameter(s): 'title'.
cursor.execute("WHERE id = ?", {"id": 42})
# TypeError: question makes use of positional placeholders (?), however dict was supplied.
cursor.execute("WHERE id = %(id)s", (42,))
# TypeError: question makes use of named placeholders (%(title)s), however tuple was supplied.
Actual-World Examples
Instance 1: Net Utility
def add_user(title, e mail):
with join(connection_string) as conn:
with conn.cursor() as cursor:
cursor.execute(
"INSERT INTO customers (title, e mail) VALUES (%(title)s, %(e mail)s)",
{"title": title, "e mail": e mail}
)
Instance 2: Batch Operations
cursor.executemany(
"INSERT INTO customers (title, age) VALUES (%(title)s, %(age)s)",
[{"name": "Alice", "age": 30}, {"name": "Bob", "age": 25}]
)
Instance 3: Monetary Transactions
def transfer_funds(from_acct, to_acct, quantity):
with join(connection_string) as conn:
with conn.cursor() as cursor:
cursor.execute(
"UPDATE accounts SET stability = stability - %(quantity)s WHERE id = %(id)s",
{"quantity": quantity, "id": from_acct}
)
cursor.execute(
"UPDATE accounts SET stability = stability + %(quantity)s WHERE id = %(id)s",
{"quantity": quantity, "id": to_acct}
)
# Automated commit on success, rollback on failure
Issues to Preserve in Thoughts
- Don’t combine kinds in a single question. Use both ? or %(title)s, not each. The motive force determines which model you’re utilizing from the parameter sort (tuple vs dict), not from the SQL textual content. If placeholders don’t match the parameter sort, you’ll get a transparent TypeError explaining the mismatch. If each placeholder varieties seem within the SQL, just one set will get substituted, resulting in parameter rely mismatches at execution time.
# Mixing kinds - raises TypeError
cursor.execute( "SELECT * FROM customers WHERE id = ? AND title = %(title)s", {"title": "Alice"} # Driver finds %(title)s but additionally sees unmatched ? )
# ODBC error: parameter rely mismatch (2 placeholders, 1 worth)
# Choose one model and use it persistently
cursor.execute( "SELECT * FROM customers WHERE id = %(id)s AND title = %(title)s", {"id": 42, "title": "Alice"} )
- Further dict keys are OK. Unused parameters are silently ignored, that is by design to allow parameter dictionary reuse throughout completely different queries.
- SQL injection protected. Each kinds use ODBC parameter binding beneath the hood. Values are by no means interpolated into the SQL string, they’re all the time safely sure by the motive force.
- Literal % in SQL. Use %% to escape should you want a literal %(…)s sample in your question textual content.
cursor.execute(
"SELECT * FROM customers WHERE title LIKE %(sample)s",
{"sample": "%alice%"} # The % contained in the VALUE is ok
)
# However should you want a literal %(...)s in SQL textual content itself, use %%
cursor.execute(
"SELECT '%%(instance)s' AS literal WHERE id = %(id)s",
{"id": 42}
)
- mssql_python.paramstyle experiences “pyformat”. The DB-API 2.0 spec solely permits a single worth for this module-level fixed. We set it to pyformat as a result of it’s the extra expressive model and the one we advocate for brand new code. However qmark is totally supported at runtime, the motive force accepts each kinds transparently based mostly on whether or not you move a tuple or a dict. Consider paramstyle = “pyformat” because the marketed default, not a limitation.
Compatibility at a Look
| Function | qmark (?) | pyformat (%(title)s) |
| cursor.execute() | ✅ | ✅ |
| cursor.executemany() | ✅ | ✅ |
| connection.execute() | ✅ | ✅ |
| Parameter reuse | ❌ | ✅ |
| Saved procedures | ✅ | ✅ |
| All SQL information varieties | ✅ | ✅ |
| Backward appropriate with qmark paramstyle | ✅ | N/A (new) |
Takeaway
Use ? for fast, easy queries. Use %(title)s for advanced, multi-parameter queries the place readability and reuse matter. You don’t have to select a aspect – use whichever suits the scenario. The motive force handles the remainder.
Whether or not you’re constructing dynamic queries, or just need extra readable SQL, twin paramstyle assist makes mssql-python work the best way you already suppose.
Attempt It and Share Your Suggestions!
We invite you to:
- Examine-out the motive force and combine it into your tasks.
- Share your ideas: Open , counsel options, and contribute to the challenge.
- Be a part of the dialog: | .
Use Python Driver with Free Azure SQL Database
You should use the Python Driver with the free model of Azure SQL Database!
✅
✅ Good for testing, growth, or studying eventualities with out incurring prices.

