psf/requests · error · ValueError

Data must not be a string.

Error message

Data must not be a string.

What it means

Raised by `_encode_files` when both `files` and `data` are supplied but `data` is a string (or bytes). In a multipart request, `data` must be a dict or list of key/value tuples so each entry can become a form field; a raw string body cannot be merged into multipart form fields, so requests rejects it rather than guessing.

Source

Thrown at src/requests/models.py:197

        else:
            return data  # type: ignore[return-value]  # unreachable for valid _t.DataType

    @staticmethod
    def _encode_files(
        files: _t.FilesType, data: _t.RawDataType | None
    ) -> tuple[bytes, str]:
        """Build the body for a multipart/form-data request.

        Will successfully encode files when passed as a dict or a list of
        tuples. Order is retained if data is a list of tuples but arbitrary
        if parameters are supplied as a dict.
        The tuples may be 2-tuples (filename, fileobj), 3-tuples (filename, fileobj, contentype)
        or 4-tuples (filename, fileobj, contentype, custom_headers).
        """
        if not files:
            raise ValueError("Files must be provided.")
        elif isinstance(data, basestring):
            raise ValueError("Data must not be a string.")

        new_fields: list[RequestField | tuple[str, bytes]] = []
        fields = to_key_val_list(data or {})
        files = to_key_val_list(files or {})

        for field, val in fields:
            if isinstance(val, basestring) or not hasattr(val, "__iter__"):
                val = [val]
            for v in val:
                if v is not None:
                    # Don't call str() on bytestrings: in Py3 it all goes wrong.
                    if not isinstance(v, bytes):
                        v = str(v)

                    new_fields.append(
                        (
                            field.decode("utf-8")
                            if isinstance(field, bytes)

View on GitHub ↗ (pinned to 414f0513c3)

Solutions

  1. Pass `data` as a dict of form fields: `requests.post(url, files=files, data={'meta': json.dumps(payload)})`.
  2. If the API expects the JSON as one part, add it to `files` as an extra part: `files={'file': fh, 'payload': (None, json.dumps(payload), 'application/json')}`.
  3. If you truly need a raw string body plus a file, the request isn't multipart — build the body manually or use a MultipartEncoder (requests-toolbelt).

Example fix

# before
requests.post(url, files={'f': open('a.bin','rb')}, data=json.dumps(meta))

# after
requests.post(url, files={'f': open('a.bin','rb')}, data={'meta': json.dumps(meta)})
Defensive patterns

Strategy: type-guard

Validate before calling

if isinstance(data, str):
    raise TypeError("when files= is used, data must be a dict/list of fields, not a string body")

Type guard

def is_multipart_safe_data(data):
    return data is None or isinstance(data, (dict, list, tuple))

Try / catch

try:
    resp = requests.post(url, files=files, data=data)
except ValueError:
    # data was a raw string alongside files; convert to field dict or drop files=
    raise

Prevention

When it happens

Trigger: `requests.post(url, files={'f': fh}, data='raw string body')` or `data=b'...'` — combining a pre-serialized body (e.g. a JSON string) with `files=`. Any call path reaching `PreparedRequest.prepare_body` with non-empty `files` and string `data`.

Common situations: Developers do `data=json.dumps(payload)` (a common pattern for JSON APIs) and then add `files=` for an upload — the serialized string collides with multipart encoding. Also migrating code from a plain POST to a file upload without converting the body to a field dict.

Related errors


AI-assisted analysis of psf/requests@414f0513c3 (2026-07-31). Data as JSON: /data/errors/380a4c0c7ed31e8f.json. Report an issue: GitHub ↗.