psf/requests · error · ValueError

Data must not be a string.

Error message

Data must not be a string.

What it means

Raised by Request._encode_files when the data argument accompanying a multipart upload is a plain string (bytes or str). For multipart encoding, data must be a mapping or list of tuples so it can be merged into the form fields; passing a raw string body is ambiguous and would corrupt the multipart structure. This is a sibling guard to the files-must-be-provided check.

Solutions

  1. Convert the string body to a dict or list of (key, value) tuples before passing as data= with files= (e.g. urllib.parse.parse_qs to a dict).
  2. If the string is genuinely the request body, drop the files= argument — you cannot mix a raw body with multipart files.
  3. Use a dict for form fields: data={'field': 'value'} alongside files={'file': f}.

Example fix

// before
requests.post(url, files=files, data='field=value')

// after
requests.post(url, files=files, data={'field': 'value'})
Defensive patterns

Strategy: validation

Validate before calling

def post_multipart(url, files, data=None):
    if isinstance(data, (str, bytes)):
        raise TypeError('data must be a dict or list of tuples, not a string, when files is set')
    return requests.post(url, files=files, data=data)

Type guard

from typing import Any

def is_form_data(data: Any) -> bool:
    return data is None or isinstance(data, (dict, list, tuple))

Try / catch

# This is a caller bug; convert data instead of catching.
if isinstance(data, str):
    from urllib.parse import parse_qs
    data = {k: v[0] for k, v in parse_qs(data).items()}
requests.post(url, files=files, data=data)

Prevention

When it happens

Trigger: Calling requests.post(url, files=files, data='a=b&c=d') with a URL-encoded string instead of a dict; passing data=request.body (a string) alongside files=; mixing the JSON/string body API with the files API.

Common situations: Migrating a call from data=string to a files= upload and forgetting to convert data to a dict; building query strings manually then adding files; library wrappers that accept 'body' generically and forward it to both data= and files=.

Related errors


AI-assisted analysis of psf/requests@8068356288 (2026-08-11). Data as JSON: /api/errors/380a4c0c7ed31e8f. Report an issue: GitHub.

Appendix: 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 8068356288)