psycopg/psycopg2 · error · TypeError
Composed.join() argument must be a string or an SQL
Error message
Composed.join() argument must be a string or an SQL
What it means
Raised by Composed.join() (lib/sql.py:152-154) when the 'joiner' argument is neither a str nor a SQL instance. join() interposes the joiner between the Composed's items; the joiner must be a literal SQL snippet (constant, unescaped) so that the result remains a safe composition. Other Composable types (Identifier, Literal, Placeholder) are rejected.
Source
Thrown at lib/sql.py:153
def join(self, joiner):
"""
Return a new `!Composed` interposing the *joiner* with the `!Composed` items.
The *joiner* must be a `SQL` or a string which will be interpreted as
an `SQL`.
Example::
>>> fields = sql.Identifier('foo') + sql.Identifier('bar') # a Composed
>>> print(fields.join(', ').as_string(conn))
"foo", "bar"
"""
if isinstance(joiner, str):
joiner = SQL(joiner)
elif not isinstance(joiner, SQL):
raise TypeError(
"Composed.join() argument must be a string or an SQL")
return joiner.join(self)
class SQL(Composable):
"""
A `Composable` representing a snippet of SQL statement.
`!SQL` exposes `join()` and `format()` methods useful to create a template
where to merge variable parts of a query (for instance field or table
names).
The *string* doesn't undergo any form of escaping, so it is not suitable to
represent variable identifiers or values: you should only use it to pass
constant strings representing templates or snippets of SQL statements; use
other objects such as `Identifier` or `Literal` to represent variable
parts.View on GitHub (pinned to 3a6d9d6ddc)
Solutions
- Pass a plain string or a sql.SQL instance as the joiner, e.g. composed.join(', ') or composed.join(sql.SQL(', ')).
- If you need identifiers separated by a dot, use sql.Identifier('schema', 'table') which handles dotted names natively.
- Double-check you are calling join() on the right class: Composed.join(separator) vs SQL.join(sequence).
Example fix
// before
fields.join(sql.Identifier(','))
// after
fields.join(', ') Defensive patterns
Strategy: type-guard
Validate before calling
if not isinstance(joiner, (str, sql.SQL)):
raise TypeError('joiner must be str or sql.SQL')
result = composed.join(joiner) Type guard
from psycopg2.sql import SQL
def is_valid_joiner(j) -> bool:
return isinstance(j, (str, SQL)) Try / catch
try:
result = composed.join(joiner)
except TypeError as e:
if 'must be a string or an SQL' in str(e):
result = composed.join(str(joiner))
else: raise Prevention
- Pass a literal separator string (e.g. ', ') to Composed.join.
- Remember Composed.join takes a separator; SQL.join takes a sequence.
When it happens
Trigger: Calling composed.join(sql.Identifier('x')), composed.join(sql.Literal(1)), composed.join(42), or composed.join(None). The method accepts a plain str only because it implicitly wraps it in SQL (line 150-151); any other type fails.
Common situations: Developers pass an Identifier as a separator (e.g. wanting 'schema.table' joins) instead of a literal. Passing a Placeholder or Literal as a joiner. Forgetting that join() is on Composed, not SQL (SQL.join accepts a sequence of any Composables).
Related errors
- Composed elements must be Composable, got {i!r} instead
- SQL values must be strings
- pgrange must be a string or a RangeAdapter strict subclass
- pyrange must be a type or a Range strict subclass
- you can't specify both 'database' and 'dbname' arguments
AI-assisted analysis of psycopg/psycopg2@3a6d9d6ddc (2026-08-04).
Data as JSON: /data/errors/7819d34832e4e32f.json.
Report an issue: GitHub.