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

  1. Rewrite the query to contain exactly one '%s' where the VALUES list should go: 'INSERT INTO t (a,b) VALUES %s'.
  2. If you need per-row placeholders, pass them via the 'template' argument (e.g. template='(%s, %s)') and keep one top-level '%s'.
  3. 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

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


AI-assisted analysis of psycopg/psycopg2@3a6d9d6ddc (2026-08-04). Data as JSON: /data/errors/e6edcc85481c6a6d.json. Report an issue: GitHub.