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

  1. Inspect what the server actually returns for `server`/`version` in the handshake (enable DEBUG logging on redis.connection) and ensure both are strings.
  2. Run a server version whose HANDSHAKE emits string `server`/`version` fields (Redis 7.4+ compliant).
  3. If running a compatible fork, patch it to emit string handshake fields.
  4. 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

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


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)