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

  1. Pass a plain string or a sql.SQL instance as the joiner, e.g. composed.join(', ') or composed.join(sql.SQL(', ')).
  2. If you need identifiers separated by a dot, use sql.Identifier('schema', 'table') which handles dotted names natively.
  3. 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

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


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