{"record":{"id":"66750a8f519d9fe7","repo":"redis/redis-py","slug":"ex-must-be-datetime-timedelta-or-int","errorCode":null,"errorMessage":"ex must be datetime.timedelta or int","messagePattern":"ex must be datetime\\.timedelta or int","errorType":"exception","errorClass":"DataError","httpStatus":null,"severity":"error","filePath":"redis/utils.py","lineNumber":374,"sourceCode":"\n\ndef extract_expire_flags(\n    ex: Optional[ExpiryT] = None,\n    px: Optional[ExpiryT] = None,\n    exat: Optional[AbsExpiryT] = None,\n    pxat: Optional[AbsExpiryT] = None,\n) -> List[EncodableT]:\n    exp_options: list[EncodableT] = []\n    if ex is not None:\n        exp_options.append(\"EX\")\n        if isinstance(ex, datetime.timedelta):\n            exp_options.append(int(ex.total_seconds()))\n        elif isinstance(ex, int):\n            exp_options.append(ex)\n        elif isinstance(ex, str) and ex.isdigit():\n            exp_options.append(int(ex))\n        else:\n            raise DataError(\"ex must be datetime.timedelta or int\")\n    elif px is not None:\n        exp_options.append(\"PX\")\n        if isinstance(px, datetime.timedelta):\n            exp_options.append(int(px.total_seconds() * 1000))\n        elif isinstance(px, int):\n            exp_options.append(px)\n        else:\n            raise DataError(\"px must be datetime.timedelta or int\")\n    elif exat is not None:\n        if isinstance(exat, datetime.datetime):\n            exat = int(exat.timestamp())\n        exp_options.extend([\"EXAT\", exat])\n    elif pxat is not None:\n        if isinstance(pxat, datetime.datetime):\n            pxat = int(pxat.timestamp() * 1000)\n        exp_options.extend([\"PXAT\", pxat])\n\n    return exp_options","sourceCodeStart":356,"sourceCodeEnd":392,"githubUrl":"https://github.com/redis/redis-py/blob/6a6b581b48225afa0b76912d1028c6035baee932/redis/utils.py#L356-L392","documentation":"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).","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"# before\nr.set('k', 'v', ex=3.5)      # DataError: ex must be datetime.timedelta or int\nr.set('k', 'v', ex='3.5')    # also raises (isdigit() is False for '3.5')\n\n# after\nimport math, datetime\nr.set('k', 'v', ex=math.ceil(3.5))               # int seconds\nr.set('k', 'v', ex=datetime.timedelta(seconds=3.5))  # timedelta\nr.set('k', 'v', ex=int(float('3.5')))            # coerce string input","handlingStrategy":"validation","validationCode":"import datetime\n\ndef coerce_ex(ex):\n    # call BEFORE r.set(..., ex=ex); mirrors utils.extract_expire_flags rules\n    if ex is None or isinstance(ex, (int, datetime.timedelta)):\n        return ex\n    if isinstance(ex, str) and ex.isdigit():\n        return int(ex)\n    raise TypeError('ex must be int, timedelta, or digit-string')","typeGuard":"import datetime\nfrom typing import Union\n\ndef is_valid_ex(ex) -> bool:\n    return (\n        ex is None\n        or isinstance(ex, int)\n        or isinstance(ex, datetime.timedelta)\n        or (isinstance(ex, str) and ex.isdigit())\n    )","tryCatchPattern":"import redis.exceptions\ntry:\n    r.set('k', 'v', ex=ttl)\nexcept redis.exceptions.DataError as e:\n    if 'ex must be' in str(e):\n        ttl = int(float(ttl))  # or math.ceil / timedelta\n        r.set('k', 'v', ex=ttl)","preventionTips":["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."],"tags":["set","expiry","data-error","validation"],"backgroundTag":null,"analyzedSha":"6a6b581b48225afa0b76912d1028c6035baee932","analyzedAt":"2026-08-10T12:52:44.840Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-21T04:17:39.646Z"}