psycopg/psycopg2 · error · ValueError

bound flags not valid: {bounds!r}

Error message

bound flags not valid: {bounds!r}

What it means

Raised by Range.__init__ when the 'bounds' argument is not one of the four valid PostgreSQL range bound flags: '[)', '(]', '()', or '[]'. These flags denote whether each endpoint is inclusive (bracket) or exclusive (parenthesis). Any other string value (including lowercase variants or reversed pairs) is rejected because it cannot be mapped to a PostgreSQL range literal.

Source

Thrown at lib/_range.py:50

from psycopg2.extensions import new_type, new_array_type, register_type


class Range:
    """Python representation for a PostgreSQL |range|_ type.

    :param lower: lower bound for the range. `!None` means unbound
    :param upper: upper bound for the range. `!None` means unbound
    :param bounds: one of the literal strings ``()``, ``[)``, ``(]``, ``[]``,
        representing whether the lower or upper bounds are included
    :param empty: if `!True`, the range is empty

    """
    __slots__ = ('_lower', '_upper', '_bounds')

    def __init__(self, lower=None, upper=None, bounds='[)', empty=False):
        if not empty:
            if bounds not in ('[)', '(]', '()', '[]'):
                raise ValueError(f"bound flags not valid: {bounds!r}")

            self._lower = lower
            self._upper = upper
            self._bounds = bounds
        else:
            self._lower = self._upper = self._bounds = None

    def __repr__(self):
        if self._bounds is None:
            return f"{self.__class__.__name__}(empty=True)"
        else:
            return "{}({!r}, {!r}, {!r})".format(self.__class__.__name__,
                self._lower, self._upper, self._bounds)

    def __str__(self):
        if self._bounds is None:
            return 'empty'

View on GitHub (pinned to 3a6d9d6ddc)

Solutions

  1. Use exactly one of '[)', '(]', '()', '[]' as the bounds argument (default is '[)').
  2. If the bounds string comes from external input, validate it against ('[)', '(]', '()', '[]') before constructing the Range.
  3. If you actually want an empty range, pass empty=True instead of trying to express emptiness via bounds.

Example fix

// before
r = Range(1, 10, bounds=']( ')
// after
r = Range(1, 10, bounds='[)')
Defensive patterns

Strategy: validation

Validate before calling

VALID_BOUNDS = ('[)', '(]', '()', '[]')
if bounds not in VALID_BOUNDS:
    raise ValueError(f'invalid bounds {bounds!r}; expected one of {VALID_BOUNDS}')
r = Range(lower, upper, bounds=bounds)

Type guard

def is_valid_bounds(b) -> bool:
    return isinstance(b, str) and b in ('[)', '(]', '()', '[]')

Try / catch

try:
    r = Range(lo, hi, bounds=b)
except ValueError as e:
    # log and fall back to default bounds
    r = Range(lo, hi)

Prevention

When it happens

Trigger: Constructing a Range (or any subclass like NumericRange, DateRange) with an explicit bounds keyword that is misspelled or wrong, e.g. Range(1, 10, bounds='[]'), Range(1, 10, bounds='[ ]'), Range(1, 10, bounds=')('), or passing a non-string object. The check at lib/_range.py:49 runs only when empty=False (the default).

Common situations: Developers confuse the bracket/parenthesis order (writing '][' or ')('), add a space inside the flag, or use uppercase. Also occurs when bounds are read from user input or a config file without validation. The empty=True path bypasses this entirely, so some users forget that non-empty ranges always require a valid flag.

Related errors


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