encode/httpx · error · TypeError
Multipart file uploads require 'io.BytesIO', not 'io.StringI
Error message
Multipart file uploads require 'io.BytesIO', not 'io.StringIO'.
What it means
Raised as `TypeError` by `FileField.__init__` when the uploaded file object is an `io.StringIO`. Multipart bodies are 8-bit clean byte streams, so file uploads must be binary (`io.BytesIO` or any `io.BufferedIOBase`). Passing a text-mode in-memory stream would force an implicit, lossy encode, so httpx rejects it.
Source
Thrown at httpx/_multipart.py:159
else:
# all 4 parameters included
filename, fileobj, content_type, headers = value # type: ignore
else:
filename = Path(str(getattr(value, "name", "upload"))).name
fileobj = value
if content_type is None:
content_type = _guess_content_type(filename)
has_content_type_header = any("content-type" in key.lower() for key in headers)
if content_type is not None and not has_content_type_header:
# note that unlike requests, we ignore the content_type provided in the 3rd
# tuple element if it is also included in the headers requests does
# the opposite (it overwrites the headerwith the 3rd tuple element)
headers["Content-Type"] = content_type
if isinstance(fileobj, io.StringIO):
raise TypeError(
"Multipart file uploads require 'io.BytesIO', not 'io.StringIO'."
)
if isinstance(fileobj, io.TextIOBase):
raise TypeError(
"Multipart file uploads must be opened in binary mode, not text mode."
)
self.filename = filename
self.file = fileobj
self.headers = headers
def get_length(self) -> int | None:
headers = self.render_headers()
if isinstance(self.file, (str, bytes)):
return len(headers) + len(to_bytes(self.file))
file_length = peek_filelike_length(self.file)View on GitHub (pinned to b5addb64f0)
Solutions
- Use `io.BytesIO`: `io.BytesIO(text.encode('utf-8'))`.
- Write the text to a real file opened in binary mode (`open(path, 'rb')`).
- If the content is already a `str`, pass `('name.txt', text)` directly (httpx will encode it).
- Pick an explicit encoding (utf-8, latin-1) when converting to avoid surprises.
Example fix
// before
client.post(url, files={'f': ('x.txt', io.StringIO('hello'))}) # TypeError
// after
client.post(url, files={'f': ('x.txt', io.BytesIO('hello'.encode('utf-8')))}) Defensive patterns
Strategy: validation
Validate before calling
import io
def to_binary_file(obj):
if isinstance(obj, io.StringIO):
return io.BytesIO(obj.getvalue().encode('utf-8'))
return obj Type guard
import io
def is_binary_filelike(obj) -> bool:
return not isinstance(obj, (io.StringIO, io.TextIOBase)) Try / catch
try:
client.post(url, files={'f': ('x.txt', StringIO('hi'))})
except TypeError:
client.post(url, files={'f': ('x.txt', io.BytesIO(b'hi'))}) Prevention
- Use io.BytesIO for in-memory uploads; encode text explicitly.
- Pass ('name', str_payload) directly if the content is already a str.
- Centralize file-payload construction so the binary requirement is enforced once.
When it happens
Trigger: Calling `client.post(url, files={'f': ('name.txt', io.StringIO('hello'))})` or handing a `StringIO` directly as the file value.
Common situations: Generating CSV/text content in memory with `StringIO` and uploading without converting; porting code that treated file content as text; building file payloads from string templates.
Related errors
- Multipart file uploads must be opened in binary mode, not te
- Invalid type for name. Expected str, got {type(name)}: {name
- Invalid type for value. Expected primitive type, got {type(v
- Invalid "auth" argument: {auth!r}
- Unexpected type for 'content', {type(content)!r}
AI-assisted analysis of encode/httpx@b5addb64f0 (2026-08-04).
Data as JSON: /data/errors/7ab7ad3b1afbef01.json.
Report an issue: GitHub.