redis/redis-py · error · DataError

px must be datetime.timedelta or int

Error message

px must be datetime.timedelta or int

What it means

Raised by extract_expire_flags() (redis/utils.py:382) as redis.exceptions.DataError. The `px` argument (relative expire in milliseconds) is accepted ONLY as datetime.timedelta or int (utils.py:377-380) — unlike `ex`, a digit-string is NOT accepted here. Any other type (float, string, list) raises DataError. A timedelta is converted via int(px.total_seconds()*1000).

Solutions

  1. Pass px as an int milliseconds: px=1500.
  2. Pass px as datetime.timedelta(milliseconds=...) or timedelta(seconds=...).
  3. Convert string input with int() before passing: px=int(cfg['px']).
  4. If you have seconds as a float, compute px=int(sec*1000) to keep millisecond precision as an int.

Example fix

# before
r.set('k', 'v', px='1500')   # DataError: px must be datetime.timedelta or int
r.set('k', 'v', px=1500.5)   # also raises (float)

# after
import datetime
r.set('k', 'v', px=int('1500'))                          # int ms
r.set('k', 'v', px=datetime.timedelta(milliseconds=1500))  # timedelta
r.set('k', 'v', px=int(1.5 * 1000))                       # coerce float sec to int ms
Defensive patterns

Strategy: validation

Validate before calling

import datetime

def coerce_px(px):
    # call BEFORE r.set(..., px=px); px does NOT accept digit-strings (unlike ex)
    if px is None or isinstance(px, (int, datetime.timedelta)):
        return px
    if isinstance(px, str):
        if px.isdigit():
            return int(px)
        raise TypeError('px string must be all digits')
    raise TypeError('px must be int or timedelta')

Type guard

import datetime

def is_valid_px(px) -> bool:
    return (
        px is None
        or isinstance(px, int)
        or isinstance(px, datetime.timedelta)
    )

Try / catch

import redis.exceptions
try:
    r.set('k', 'v', px=ms_ttl)
except redis.exceptions.DataError as e:
    if 'px must be' in str(e):
        ms_ttl = int(float(ms_ttl))  # px rejects strings/floats; coerce to int
        r.set('k', 'v', px=ms_ttl)

Prevention

When it happens

Trigger: Calling a SET-family command with px=1500.5 (float), px='1500' (string — note: ex accepts digit-strings but px does NOT, a common surprise), px=[1500], or any non-int/non-timedelta value. The `else` branch at utils.py:381 fires.

Common situations: Reusing a string-typed config value for px ('1500' instead of 1500); passing a float millisecond value; assuming px accepts the same types as ex; computing px from a float seconds-to-ms multiplication without casting to int.

Related errors


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

Appendix: source

Thrown at redis/utils.py:382

    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


def truncate_text(txt, max_length=100):
    return textwrap.shorten(
        text=txt, width=max_length, placeholder="...", break_long_words=True
    )

View on GitHub (pinned to 6a6b581b48)