psycopg/psycopg2 · error · TypeError

Composed elements must be Composable, got {i!r} instead

Error message

Composed elements must be Composable, got {i!r} instead

What it means

Raised by Composed.__init__ (lib/sql.py:107-109) when any element in the sequence passed to Composed(...) is not an instance of Composable. Composed is a composition of Composable parts (SQL, Identifier, Literal, Placeholder, or another Composed); raw strings, bytes, numbers, or None are rejected because they would bypass proper escaping and produce unsafe or malformed SQL.

Source

Thrown at lib/sql.py:108

    The object is usually created using `!Composable` operators and methods.
    However it is possible to create a `!Composed` directly specifying a
    sequence of `!Composable` as arguments.

    Example::

        >>> comp = sql.Composed(
        ...     [sql.SQL("insert into "), sql.Identifier("table")])
        >>> print(comp.as_string(conn))
        insert into "table"

    `!Composed` objects are iterable (so they can be used in `SQL.join` for
    instance).
    """
    def __init__(self, seq):
        wrapped = []
        for i in seq:
            if not isinstance(i, Composable):
                raise TypeError(
                    f"Composed elements must be Composable, got {i!r} instead")
            wrapped.append(i)

        super().__init__(wrapped)

    @property
    def seq(self):
        """The list of the content of the `!Composed`."""
        return list(self._wrapped)

    def as_string(self, context):
        rv = []
        for i in self._wrapped:
            rv.append(i.as_string(context))
        return ''.join(rv)

    def __iter__(self):
        return iter(self._wrapped)

View on GitHub (pinned to 3a6d9d6ddc)

Solutions

  1. Wrap every element in the appropriate Composable: sql.SQL for constant snippets, sql.Identifier for names, sql.Literal for values, sql.Placeholder for parameters.
  2. Use sql.SQL(', ').join([...]) which accepts Composables and is the idiomatic way to build lists.
  3. Never put raw user input directly into a Composed; always go through Literal or a placeholder.

Example fix

// before
comp = sql.Composed(["SELECT * FROM ", sql.Identifier('t')])
// after
comp = sql.Composed([sql.SQL("SELECT * FROM "), sql.Identifier('t')])
Defensive patterns

Strategy: type-guard

Validate before calling

for el in seq:
    assert isinstance(el, Composable), f'element {el!r} is not Composable'
comp = sql.Composed(seq)

Type guard

from psycopg2.sql import Composable
def all_composable(seq) -> bool:
    return all(isinstance(x, Composable) for x in seq)

Try / catch

try:
    comp = sql.Composed(seq)
except TypeError as e:
    if 'must be Composable' in str(e):
        seq = [sql.SQL(x) if isinstance(x, str) else x for x in seq]
        comp = sql.Composed(seq)
    else: raise

Prevention

When it happens

Trigger: Constructing sql.Composed(["SELECT", sql.Identifier('t')]) or sql.Composed([sql.SQL('a'), 'b', 1]). Also triggered indirectly via Composable.__add__ (which builds Composed) and SQL.format() which assembles a Composed from template parts.

Common situations: Developers mix raw strings into Composed lists expecting automatic wrapping, or pass user-supplied values directly instead of wrapping them with sql.Literal. Confusion between sql.SQL (constant template, no escaping) and sql.Literal (escaped value).

Related errors


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