psycopg/psycopg2 · error · ValueError
the query doesn't contain any '%s' placeholder
Error message
the query doesn't contain any '%s' placeholder
What it means
Raised by _split_sql() (lib/extras.py:1332-1333) when the query passed to execute_values contains zero '%s' placeholders. execute_values needs exactly one positional '%s' to mark where the VALUES list is substituted; without it there is nothing to expand and the call is meaningless.
Source
Thrown at lib/extras.py:1333
for token in tokens:
if len(token) != 2 or token[:1] != b'%':
curr.append(token)
continue
if token[1:] == b's':
if curr is pre:
curr = post
else:
raise ValueError(
"the query contains more than one '%s' placeholder")
elif token[1:] == b'%':
curr.append(b'%')
else:
raise ValueError("unsupported format character: '%s'"
% token[1:].decode('ascii', 'replace'))
if curr is pre:
raise ValueError("the query doesn't contain any '%s' placeholder")
return pre, post
# ascii except alnum and underscore
_re_clean = _re.compile(
'[' + _re.escape(' !"#$%&\'()*+,-./:;<=>?@[\\]^`{|}~') + ']')
View on GitHub (pinned to 3a6d9d6ddc)
Solutions
- Rewrite the query to contain exactly one '%s' where the VALUES list should go: 'INSERT INTO t (a,b) VALUES %s'.
- If you need per-row placeholders, pass them via the 'template' argument (e.g. template='(%s, %s)') and keep one top-level '%s'.
- For named parameters, use template='(%(a)s, %(b)s)' and keep a single '%s' in the base query.
Example fix
// before execute_values(cur, "INSERT INTO t (a,b) VALUES (%s, %s)", data) // after execute_values(cur, "INSERT INTO t (a,b) VALUES %s", data) # template auto-built
Defensive patterns
Strategy: validation
Validate before calling
assert sql.count('%s') >= 1, 'execute_values requires exactly one %s placeholder' Type guard
def has_values_placeholder(sql: str) -> bool:
import re
return any(t == '%s' for t in re.findall(r'%.', sql)) Try / catch
try:
execute_values(cur, sql, data)
except ValueError as e:
if "doesn't contain any" in str(e):
sql = sql.replace('VALUES', 'VALUES %s', 1)
else: raise Prevention
- Write execute_values queries as '... VALUES %s ...' with exactly one placeholder.
- Use the template argument for the per-tuple placeholder pattern.
When it happens
Trigger: Calling execute_values(cur, "INSERT INTO t (a,b) VALUES (%s, %s)", data) — here both '%s' are inside parentheses but there is no top-level single placeholder; or passing a query that uses named placeholders only (e.g. "VALUES (%(a)s, %(b)s)") without a '%s' anchor.
Common situations: Developers pass a fully-formed query intended for cursor.executemany, forgetting that execute_values expects a single '%s' to be expanded. Also happens when the VALUES clause is omitted entirely or the query was written for named parameters.
Related errors
- the query contains more than one '%s' placeholder
- unsupported format character: '%s'
- bound flags not valid: {bounds!r}
- DictCursorBase can't be instantiated without a row factory.
- Composed elements must be Composable, got {i!r} instead
AI-assisted analysis of psycopg/psycopg2@3a6d9d6ddc (2026-08-04).
Data as JSON: /data/errors/e6edcc85481c6a6d.json.
Report an issue: GitHub.