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
- Pass px as an int milliseconds: px=1500.
- Pass px as datetime.timedelta(milliseconds=...) or timedelta(seconds=...).
- Convert string input with int() before passing: px=int(cfg['px']).
- 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
- Pass px as int milliseconds only — it rejects strings and floats (unlike ex).
- Convert any string/float ttl to int ms before the SET call.
- Prefer datetime.timedelta(milliseconds=...) for readable, type-safe expiry.
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
- ex must be datetime.timedelta or int
- ``enx`` requires one of ``ex``, ``px``, ``exat``, or…
- ``ex``, ``px``, ``exat``, ``pxat``, and ``keepttl`` are…
- ``ex``, ``px``, ``exat``, ``pxat``, and ``persist`` are…
- HIMPORT fields must be a collection of field names, not a…
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)