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
- If the payload is already bytes, use `body=`: `Response(body=orjson.dumps(data), content_type='application/json')`.
- If you have bytes that represent text, decode first: `Response(text=b'hi'.decode('utf-8'))`.
- Coerce known types: `Response(text=str(value))` only when you genuinely mean the string form.
- 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
- Use body= for pre-encoded bytes (e.g. orjson output).
- Decode bytes to str before passing to text=.
- For JSON, prefer json_response() which handles encoding.
- Type-annotate helpers so mypy catches non-str text early.
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
- body and text are not allowed together
- charset must not be in content_type argument
- passing both Content-Type header and content_type or…
- Unsupported etag type
- Unsupported type for last_modified
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_typeView on GitHub (pinned to d041d4d0fd)