encode/httpx · error · TypeError
Multipart file uploads must be opened in binary mode, not te
Error message
Multipart file uploads must be opened in binary mode, not text mode.
What it means
Raised as `TypeError` by `FileField.__init__` when the file object is an instance of `io.TextIOBase` — i.e. a file opened in text mode (`open(path, 'r')`). Multipart uploads require binary file objects because httpx reads raw bytes; a text-mode file would require decoding/encoding assumptions httpx will not make.
Source
Thrown at httpx/_multipart.py:163
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)
# If we can't determine the filesize without reading it into memory,
# then return `None` here, to indicate an unknown file length.
if file_length is None:View on GitHub (pinned to b5addb64f0)
Solutions
- Open in binary mode: `open(path, 'rb')`.
- Use `pathlib.Path(path).read_bytes()` to get bytes directly, then pass a BytesIO or the bytes.
- Prefer `with open(path, 'rb') as f:` to scope the handle correctly.
- On Windows, always pass `'rb'`/`'wb'` for network payloads to avoid CRLF translation.
Example fix
// before
client.post(url, files={'f': open('data.csv', 'r')}) # TypeError
// after
with open('data.csv', 'rb') as f:
client.post(url, files={'f': ('data.csv', f)}) Defensive patterns
Strategy: validation
Validate before calling
import io
def assert_binary(fileobj) -> None:
if isinstance(fileobj, io.TextIOBase):
raise TypeError(f'{fileobj!r} opened in text mode; reopen with mode="rb"') Type guard
import io
def is_binary_mode(fileobj) -> bool:
return not isinstance(fileobj, io.TextIOBase) Try / catch
try:
client.post(url, files={'f': open(path, 'r')})
except TypeError:
with open(path, 'rb') as f:
client.post(url, files={'f': (path, f)}) Prevention
- Always open files for upload with mode='rb'.
- On Windows, binary mode also avoids unwanted CRLF translation.
- Prefer pathlib.Path.read_bytes() for small uploads.
When it happens
Trigger: Calling `client.post(url, files={'f': open('data.csv', 'r')})` — note the `'r'` mode. The same file opened with `'rb'` works fine.
Common situations: Reusing a file handle that was opened for reading as text earlier in the program; platform-default mode on Windows where text mode performs CR/LF translation; copy-pasted examples that omit the `b`.
Related errors
- Multipart file uploads require 'io.BytesIO', not 'io.StringI
- 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/70e2ba2123cde389.json.
Report an issue: GitHub.