{"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":"validation","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/da03cdc7e8731092b13e395605c3c1fb2de25de1/redis/utils.py#L356-L392","documentation":"Raised by extract_expire_flags (redis/utils.py:374) as redis.exceptions.DataError when the `ex` (relative expiry in seconds) argument is not a datetime.timedelta, an int, or a numeric string. The function explicitly handles timedelta (converted via total_seconds), int, and str.isdigit() values; any other type (float, bool, None is allowed at the top-level check, list, object) triggers DataError. Used to assemble EX expiry flags for commands like SET.","triggerScenarios":"Calling client.set('k', 'v', ex=1.5) (float not allowed), client.set('k', 'v', ex=True), or ex=<some_object>. Note ex='100' (numeric string) is accepted but ex='1.5' is not (isdigit is False).","commonSituations":"Passing a float for sub-second expiry (use px instead); passing a bool by accident; passing a Decimal or numpy integer; computing ex from a division that yields a float.","solutions":["Use an int: client.set('k', 'v', ex=int(value)).","For sub-second precision use px (milliseconds): client.set('k', 'v', px=int(value * 1000)).","Use datetime.timedelta for clarity: client.set('k', 'v', ex=datetime.timedelta(seconds=value)).","Round/validate the value before passing: ex = max(1, int(round(value)))."],"exampleFix":"# before\nclient.set('k', 'v', ex=1.5)   # float -> DataError\n# after\nclient.set('k', 'v', px=1500)  # milliseconds, sub-second safe\n# or\nimport datetime\nclient.set('k', 'v', ex=datetime.timedelta(seconds=1.5))","handlingStrategy":"type-guard","validationCode":"import datetime\nfrom typing import Any, Union\n\ndef coerce_ex(ex: Any) -> Union[int, datetime.timedelta, None]:\n    if ex is None:\n        return None\n    if isinstance(ex, (datetime.timedelta, int)):\n        return ex\n    if isinstance(ex, str) and ex.isdigit():\n        return int(ex)\n    if isinstance(ex, float):\n        return int(ex)          # or convert to timedelta for sub-second\n    raise TypeError('ex must be timedelta, int, or numeric string')\n\nclient.set('k', 'v', ex=coerce_ex(user_value))","typeGuard":"import datetime\nfrom typing import Any\n\ndef is_valid_ex(value: Any) -> bool:\n    return (\n        value is None\n        or isinstance(value, (datetime.timedelta, int))\n        or (isinstance(value, str) and value.isdigit())\n    )","tryCatchPattern":"from redis.exceptions import DataError\n\ntry:\n    client.set('k', 'v', ex=value)\nexcept DataError:\n    client.set('k', 'v', ex=int(value))   # or px=int(value*1000)","preventionTips":["Always pass int or datetime.timedelta for ex; never a raw float.","For sub-second TTLs, prefer px (milliseconds).","Validate expiry values at the configuration layer before they reach Redis calls.","Add a thin wrapper around set() that normalizes TTL arguments."],"tags":["types","validation","expiry","commands","set"],"analyzedSha":"da03cdc7e8731092b13e395605c3c1fb2de25de1","analyzedAt":"2026-08-04T20:26:47.563Z","schemaVersion":2}