redis/redis-py · error · TypeError
Key must be either a string or bytes
Error message
Key must be either a string or bytes
What it means
Raised by ensure_string() (redis/utils.py:355) as a TypeError. ensure_string() normalizes a value to str: bytes are decoded to utf-8, str passes through, and anything else raises TypeError. In the shipped code it is called by the client-side-caching connection wrapper (redis/connection.py:1736-1737) on the `server` and `version` fields pulled from the Redis HANDSHAKE metadata. A None value is already excluded at connection.py:1733, so this error means the handshake metadata returned a non-string, non-bytes scalar such as an int, bool, or a structured RESP3 object.
Solutions
- Inspect what the server actually returns for `server`/`version` in the handshake (enable DEBUG logging on redis.connection) and ensure both are strings.
- Run a server version whose HANDSHAKE emits string `server`/`version` fields (Redis 7.4+ compliant).
- If running a compatible fork, patch it to emit string handshake fields.
- Avoid enabling client-side caching against servers with non-conforming handshakes.
Example fix
// before: server returns version as integer in handshake metadata // -> TypeError: Key must be either a string or bytes (utils.py:355) // after: server-side fix - emit HANDSHAKE version as a bulk string // server: arr *2 $7 server $5 redis $7 version $5 7.4.0
Defensive patterns
Strategy: type-guard
Validate before calling
def handshake_fields_are_strings(client) -> bool:
md = getattr(getattr(client, 'connection_pool', None), 'handshake_metadata', None)
if not md:
return True # nothing to check yet
for key in (b'server', 'server', b'version', 'version'):
v = md.get(key)
if v is not None and not isinstance(v, (str, bytes)):
return False
return True Type guard
from typing import Union
def is_str_or_bytes(v) -> bool:
return isinstance(v, (str, bytes))
# equivalent to the guard ensure_string() expects:
# assert is_str_or_bytes(handshake['version']), 'non-string handshake field'
# assert is_str_or_bytes(handshake['server']), 'non-string handshake field' Try / catch
import redis
try:
c = redis.Redis(host=..., client_side_caching=True, protocol=3)
c.ping()
except TypeError as e:
if 'must be either a string or bytes' in str(e):
# server returned non-string server/version in HANDSHAKE metadata;
# use a Redis 7.4+ compliant server or disable client-side caching
... Prevention
- Use a Redis 7.4+ (or compliant fork) server whose HANDSHAKE emits string fields.
- Smoke-test client_side_caching=True against your exact server build in staging before prod.
- If unsure of server handshake shape, leave client-side caching off until verified.
When it happens
Trigger: Connecting with client-side caching enabled (Redis 7.4+ CSC, protocol=3), the HANDSHAKE metadata's `server` or `version` field arrives decoded as a non-string type (e.g. an integer, a boolean, or a nested aggregate from a non-conforming RESP3 parser path); ensure_string() then raises TypeError before the version-compatibility check at connection.py:1739-1745.
Common situations: Pointing the CSC client at a Redis fork / proxy / Valkey variant that emits `version` as a numeric or structured value; a RESP3 decoder change that returns the handshake field as a non-string; a custom or in-house server that reshapes the HANDSHAKE `server`/`version` entries.
Related errors
- Cannot retrieve information about server version
- Client caching is only supported with RESP version 3
- Client caching is only supported with RESP version 3
- Client caching is only supported with RESP version 3
- Cache must implement CacheInterface
AI-assisted analysis of redis/redis-py@6a6b581b48 (2026-08-10).
Data as JSON: /api/errors/b16da8bde9cf8249.
Report an issue: GitHub.
Appendix: source
Thrown at redis/utils.py:355
for _ in range(diff):
num_versions1.append(0)
for i, ver in enumerate(num_versions1):
if num_versions1[i] > num_versions2[i]:
return -1
elif num_versions1[i] < num_versions2[i]:
return 1
return 0
def ensure_string(key):
if isinstance(key, bytes):
return key.decode("utf-8")
elif isinstance(key, str):
return key
else:
raise TypeError("Key must be either a string or bytes")
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:View on GitHub (pinned to 6a6b581b48)