redis/redis-py · error · DataError

``ex``, ``px``, ``exat``, ``pxat``, and ``keepttl`` are mutu

Error message

``ex``, ``px``, ``exat``, ``pxat``, and ``keepttl`` are mutually exclusive.

What it means

msetex() (Redis 8.4+) sets multiple keys with an optional expiry policy. ex, px, exat, pxat, and keepttl are mutually exclusive because a key can only have one TTL directive. The at_most_one_value_set guard at core.py:3875 raises DataError when two or more are set.

Source

Thrown at redis/commands/core.py:3884

            specified in unix time.

        ``keepttl`` if True, retain the time to live associated with the keys.

        Returns the number of fields that were added.

        Available since Redis 8.4
        For more information, see https://redis.io/commands/msetex
        """
        if not at_most_one_value_set(
            (
                ex is not None,
                px is not None,
                exat is not None,
                pxat is not None,
                keepttl,
            )
        ):
            raise DataError(
                "``ex``, ``px``, ``exat``, ``pxat``, "
                "and ``keepttl`` are mutually exclusive."
            )

        exp_options: list[EncodableT] = []
        if data_persist_option:
            exp_options.append(data_persist_option.value)

        exp_options.extend(extract_expire_flags(ex, px, exat, pxat))

        if keepttl:
            exp_options.append("KEEPTTL")

        pieces = ["MSETEX", len(mapping)]

        for pair in mapping.items():
            pieces.extend(pair)

View on GitHub (pinned to da03cdc7e8)

Solutions

  1. Pass exactly one of ex/px/exat/pxat/keepttl, e.g. r.msetex(mapping, ex=60).
  2. Use keepttl=True alone to retain existing TTLs on overwritten keys.
  3. Build a single 'choose one expiry' resolver shared across all set-family calls.

Example fix

# before
r.msetex({'a': '1', 'b': '2'}, ex=60, keepttl=True)

# after
r.msetex({'a': '1', 'b': '2'}, ex=60)
Defensive patterns

Strategy: validation

Validate before calling

expiry = {'ex': ex, 'px': px, 'exat': exat, 'pxat': pxat}
if keepttl:
    expiry['keepttl'] = True
if sum(bool(v) for v in expiry.values()) > 1:
    raise ValueError('msetex: pass exactly one expiry option')
r.msetex(mapping, **expiry)

Type guard

def valid_msetex_expiry(ex, px, exat, pxat, keepttl) -> bool:
    return sum(bool(x) for x in (ex, px, exat, pxat, keepttl)) <= 1

Try / catch

from redis.exceptions import DataError
try:
    r.msetex(mapping, ex=ex, keepttl=keepttl)
except DataError as e:
    if 'mutually exclusive' in str(e):
        r.msetex(mapping, ex=ex)
    else:
        raise

Prevention

When it happens

Trigger: r.msetex({k1:v1, k2:v2}, ex=60, keepttl=True) or any pair among the five options. data_persist_option (NX/XX) is independent and not part of this check.

Common situations: Reusing an expiry-config object across set/msetex that includes keepttl alongside a relative expiry; migrating from set() (which uses keepttl) to msetex() (which treats keepttl as exclusive).

Related errors


AI-assisted analysis of redis/redis-py@da03cdc7e8 (2026-08-04). Data as JSON: /data/errors/c7117076d08a332a.json. Report an issue: GitHub.