psycopg/psycopg2 · error · InterfaceError

failed to parse range: '{s}'

Error message

failed to parse range: '{s}'

What it means

Raised by RangeCaster.parse() (lib/_range.py:441-443) when a string returned by PostgreSQL for a range column does not match the expected range literal grammar (regex at lib/_range.py:418-430). PostgreSQL range output looks like '[1,10)', '(,5]', 'empty', or '["a","z")'. If the server sends malformed data or the typecaster is misregistered, parsing fails.

Source

Thrown at lib/_range.py:443

        (?:                         # upper bound:
          " ( (?: [^"] | "")* ) "   #   - a quoted string
          | ( [^"\)\]]+ )           #   - or an unquoted string
        )?                          #   - or empty (not catched)
        ( \)|\] )                   # upper bound flag
        """, re.VERBOSE)

    _re_undouble = re.compile(r'(["\\])\1')

    def parse(self, s, cur=None):
        if s is None:
            return None

        if s == 'empty':
            return self.range(empty=True)

        m = self._re_range.match(s)
        if m is None:
            raise InterfaceError(f"failed to parse range: '{s}'")

        lower = m.group(3)
        if lower is None:
            lower = m.group(2)
            if lower is not None:
                lower = self._re_undouble.sub(r"\1", lower)

        upper = m.group(5)
        if upper is None:
            upper = m.group(4)
            if upper is not None:
                upper = self._re_undouble.sub(r"\1", upper)

        if cur is not None:
            lower = cur.cast(self.subtype_oid, lower)
            upper = cur.cast(self.subtype_oid, upper)

        bounds = m.group(1) + m.group(6)

View on GitHub (pinned to 3a6d9d6ddc)

Solutions

  1. Confirm the column is actually a range type and that the caster's oid matches it.
  2. Register casters with connection-scoped (not global) scope to avoid oid collisions across databases.
  3. Re-register the range type after a restore/migration that changes OIDs.
  4. Inspect the raw value with the default caster to see what the server actually sent.

Example fix

// before
# global caster with wrong oid intercepts a text column
caster = RangeCaster('myrange', MyRange, oid=25, subtype_oid=25)
caster._register()  # oid 25 is text, not a range
// after
# register only on the specific connection with the correct oid
caster = register_range('myrange', MyRange, conn)
Defensive patterns

Strategy: try-catch

Validate before calling

from psycopg2 import InterfaceError
# No pre-call validation; parse() runs at fetch time. Verify caster oid matches column:
with conn.cursor() as c:
    c.execute("SELECT atttypid FROM pg_attribute WHERE attrelid=%s::regclass AND attname=%s", (tbl, col))
    assert c.fetchone()[0] == expected_oid

Try / catch

try:
    rows = cur.fetchall()
except InterfaceError as e:
    if 'failed to parse range' in str(e):
        # re-register the caster with the correct oid, then re-query
        pass
    else: raise

Prevention

When it happens

Trigger: Fetching from a range column whose value cannot be parsed by _re_range. This is almost always a misregistration (wrong subtype oid mapping a non-range column to a RangeCaster) rather than genuinely corrupt server data, because PostgreSQL itself validates range output.

Common situations: Manually creating a RangeCaster with the wrong oid that collides with another type, causing non-range strings to be routed to parse(). Also seen after a pg_dump/restore that changes OIDs while stale typecasters remain registered globally.

Related errors


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