aio-libs/aiohttp · error · TypeError

text argument must be str (%r)

Error message

text argument must be str (%r)

What it means

Raised by the Response constructor when `text` is provided but is not a str instance (e.g. bytes, int, None-ish sentinel that bypassed the None check, a custom __str__ object). The constructor needs to call `text.encode(charset)`, which only works on str. Passing bytes defeats the purpose of `text=` (use `body=` instead).

Solutions

  1. If the payload is already bytes, use `body=`: `Response(body=orjson.dumps(data), content_type='application/json')`.
  2. If you have bytes that represent text, decode first: `Response(text=b'hi'.decode('utf-8'))`.
  3. Coerce known types: `Response(text=str(value))` only when you genuinely mean the string form.
  4. Use `aiohttp.web.json_response(data=data)` which handles encoding internally.

Example fix

// before
return Response(text=orjson.dumps(payload))  # bytes -> TypeError

// after
return Response(body=orjson.dumps(payload), content_type='application/json')
Defensive patterns

Strategy: type-guard

Validate before calling

def coerce_text(text):
    if isinstance(text, bytes):
        return text.decode('utf-8')
    if not isinstance(text, (str, type(None))):
        return str(text)
    return text

return Response(text=coerce_text(value))

Type guard

def is_str_text(text) -> bool:
    return text is None or isinstance(text, str)

Prevention

When it happens

Trigger: Passing `Response(text=b'hello')` (bytes), `Response(text=42)`, or `Response(text=some_object)` where the object is not a str. Common when a JSON serializer returns bytes (orjson) and the result is forwarded as text.

Common situations: Using orjson/mujson/ujso which return bytes and passing the encoded output to `text=` instead of `body=`. Forwarding an int/float status or computed value. Helpers that accept `Union[str, bytes]` and forward both to `text=`.

Related errors


AI-assisted analysis of aio-libs/aiohttp@d041d4d0fd (2026-08-11). Data as JSON: /api/errors/1955cdb94cd2a459. Report an issue: GitHub.

Appendix: source

Thrown at aiohttp/web_response.py:576

            real_headers: CIMultiDict[str] = CIMultiDict()
        else:
            real_headers = CIMultiDict(headers)

        if content_type is not None and "charset" in content_type:
            raise ValueError("charset must not be in content_type argument")

        if text is not None:
            if hdrs.CONTENT_TYPE in real_headers:
                if content_type or charset:
                    raise ValueError(
                        "passing both Content-Type header and "
                        "content_type or charset params "
                        "is forbidden"
                    )
            else:
                # fast path for filling headers
                if not isinstance(text, str):
                    raise TypeError("text argument must be str (%r)" % type(text))
                if content_type is None:
                    content_type = "text/plain"
                if charset is None:
                    charset = "utf-8"
                real_headers[hdrs.CONTENT_TYPE] = content_type + "; charset=" + charset
                body = text.encode(charset)
                text = None
        elif hdrs.CONTENT_TYPE in real_headers:
            if content_type is not None or charset is not None:
                raise ValueError(
                    "passing both Content-Type header and "
                    "content_type or charset params "
                    "is forbidden"
                )
        elif content_type is not None:
            if charset is not None:
                content_type += "; charset=" + charset
            real_headers[hdrs.CONTENT_TYPE] = content_type

View on GitHub (pinned to d041d4d0fd)