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

  1. Open in binary mode: `open(path, 'rb')`.
  2. Use `pathlib.Path(path).read_bytes()` to get bytes directly, then pass a BytesIO or the bytes.
  3. Prefer `with open(path, 'rb') as f:` to scope the handle correctly.
  4. 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

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


AI-assisted analysis of encode/httpx@b5addb64f0 (2026-08-04). Data as JSON: /data/errors/70e2ba2123cde389.json. Report an issue: GitHub.