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

  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: /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)