encode/httpx · error · TypeError
Multipart file uploads must be opened in binary mode, not…
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.
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…
- Invalid type for name. Expected str, got
- Invalid type for value. Expected primitive type, got
- Argument must be but got
- For absolute URLs, path must be empty or begin with '/'
AI-assisted analysis of encode/httpx@b5addb64f0 (2026-08-04).
Data as JSON: /api/errors/70e2ba2123cde389.
Report an issue: GitHub.
Appendix: 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)