redis/redis-py · error · DataError

ex must be datetime.timedelta or int

Error message

ex must be datetime.timedelta or int

What it means

Raised by extract_expire_flags (redis/utils.py:374) as redis.exceptions.DataError when the `ex` (relative expiry in seconds) argument is not a datetime.timedelta, an int, or a numeric string. The function explicitly handles timedelta (converted via total_seconds), int, and str.isdigit() values; any other type (float, bool, None is allowed at the top-level check, list, object) triggers DataError. Used to assemble EX expiry flags for commands like SET.

Source

Thrown at redis/utils.py:374


def extract_expire_flags(
    ex: Optional[ExpiryT] = None,
    px: Optional[ExpiryT] = None,
    exat: Optional[AbsExpiryT] = None,
    pxat: Optional[AbsExpiryT] = None,
) -> List[EncodableT]:
    exp_options: list[EncodableT] = []
    if ex is not None:
        exp_options.append("EX")
        if isinstance(ex, datetime.timedelta):
            exp_options.append(int(ex.total_seconds()))
        elif isinstance(ex, int):
            exp_options.append(ex)
        elif isinstance(ex, str) and ex.isdigit():
            exp_options.append(int(ex))
        else:
            raise DataError("ex must be datetime.timedelta or int")
    elif px is not None:
        exp_options.append("PX")
        if isinstance(px, datetime.timedelta):
            exp_options.append(int(px.total_seconds() * 1000))
        elif isinstance(px, int):
            exp_options.append(px)
        else:
            raise DataError("px must be datetime.timedelta or int")
    elif exat is not None:
        if isinstance(exat, datetime.datetime):
            exat = int(exat.timestamp())
        exp_options.extend(["EXAT", exat])
    elif pxat is not None:
        if isinstance(pxat, datetime.datetime):
            pxat = int(pxat.timestamp() * 1000)
        exp_options.extend(["PXAT", pxat])

    return exp_options

View on GitHub (pinned to da03cdc7e8)

Solutions

  1. Use an int: client.set('k', 'v', ex=int(value)).
  2. For sub-second precision use px (milliseconds): client.set('k', 'v', px=int(value * 1000)).
  3. Use datetime.timedelta for clarity: client.set('k', 'v', ex=datetime.timedelta(seconds=value)).
  4. Round/validate the value before passing: ex = max(1, int(round(value))).

Example fix

# before
client.set('k', 'v', ex=1.5)   # float -> DataError
# after
client.set('k', 'v', px=1500)  # milliseconds, sub-second safe
# or
import datetime
client.set('k', 'v', ex=datetime.timedelta(seconds=1.5))
Defensive patterns

Strategy: type-guard

Validate before calling

import datetime
from typing import Any, Union

def coerce_ex(ex: Any) -> Union[int, datetime.timedelta, None]:
    if ex is None:
        return None
    if isinstance(ex, (datetime.timedelta, int)):
        return ex
    if isinstance(ex, str) and ex.isdigit():
        return int(ex)
    if isinstance(ex, float):
        return int(ex)          # or convert to timedelta for sub-second
    raise TypeError('ex must be timedelta, int, or numeric string')

client.set('k', 'v', ex=coerce_ex(user_value))

Type guard

import datetime
from typing import Any

def is_valid_ex(value: Any) -> bool:
    return (
        value is None
        or isinstance(value, (datetime.timedelta, int))
        or (isinstance(value, str) and value.isdigit())
    )

Try / catch

from redis.exceptions import DataError

try:
    client.set('k', 'v', ex=value)
except DataError:
    client.set('k', 'v', ex=int(value))   # or px=int(value*1000)

Prevention

When it happens

Trigger: Calling client.set('k', 'v', ex=1.5) (float not allowed), client.set('k', 'v', ex=True), or ex=<some_object>. Note ex='100' (numeric string) is accepted but ex='1.5' is not (isdigit is False).

Common situations: Passing a float for sub-second expiry (use px instead); passing a bool by accident; passing a Decimal or numpy integer; computing ex from a division that yields a float.

Related errors


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