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
- Pass ex as an int: round/ceil first, e.g. ex=math.ceil(seconds).
- Pass ex as datetime.timedelta(seconds=...).
- If your input is a string, ensure it is all-digits or convert with int() before passing.
- 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
- Always pass ex as int seconds or datetime.timedelta; coerce floats with math.ceil().
- When ttl comes from user/config input, normalize it to int before calling SET.
- Remember ex (seconds) accepts digit-strings but px (ms) does NOT — be consistent.
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
- px 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/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_optionsView on GitHub (pinned to 6a6b581b48)