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

  1. Use `io.BytesIO`: `io.BytesIO(text.encode('utf-8'))`.
  2. Write the text to a real file opened in binary mode (`open(path, 'rb')`).
  3. If the content is already a `str`, pass `('name.txt', text)` directly (httpx will encode it).
  4. 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

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


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