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. The `ex` argument (relative expire in seconds) used by SET-family commands is accepted as datetime.timedelta, int, or a string of digits (parsed via int()); see utils.py:367-373. Any other type — float, non-digit string, list, etc. — raises DataError before any command is sent to Redis. extract_expire_flags() is called from SET/SETEX/INCREX and similar commands (commands/core.py:3288, 3519, 3893, 4452, 9881, 10118).

Solutions

  1. Pass ex as an int: round/ceil first, e.g. ex=math.ceil(seconds).
  2. Pass ex as datetime.timedelta(seconds=...).
  3. If your input is a string, ensure it is all-digits or convert with int() before passing.
  4. Use px (milliseconds) as an int if you need sub-second precision.

Example fix

# before
r.set('k', 'v', ex=3.5)      # DataError: ex must be datetime.timedelta or int
r.set('k', 'v', ex='3.5')    # also raises (isdigit() is False for '3.5')

# after
import math, datetime
r.set('k', 'v', ex=math.ceil(3.5))               # int seconds
r.set('k', 'v', ex=datetime.timedelta(seconds=3.5))  # timedelta
r.set('k', 'v', ex=int(float('3.5')))            # coerce string input
Defensive patterns

Strategy: validation

Validate before calling

import datetime

def coerce_ex(ex):
    # call BEFORE r.set(..., ex=ex); mirrors utils.extract_expire_flags rules
    if ex is None or isinstance(ex, (int, datetime.timedelta)):
        return ex
    if isinstance(ex, str) and ex.isdigit():
        return int(ex)
    raise TypeError('ex must be int, timedelta, or digit-string')

Type guard

import datetime
from typing import Union

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

Try / catch

import redis.exceptions
try:
    r.set('k', 'v', ex=ttl)
except redis.exceptions.DataError as e:
    if 'ex must be' in str(e):
        ttl = int(float(ttl))  # or math.ceil / timedelta
        r.set('k', 'v', ex=ttl)

Prevention

When it happens

Trigger: Calling a SET-family command with ex=3.5 (float), ex='3.5' (string with a decimal point — str.isdigit() is False), ex='abc', ex=[10], etc. A frequent slip is passing a float seconds value or a numeric string that contains a decimal point.

Common situations: Passing milliseconds where seconds are expected as a float; user input parsed as a decimal string; computing ex from a division that yields a float; reusing a string-typed config value that contains a decimal.

Related errors


AI-assisted analysis of redis/redis-py@6a6b581b48 (2026-08-10). Data as JSON: /api/errors/66750a8f519d9fe7. Report an issue: GitHub.

Appendix: 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 6a6b581b48)