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
- 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).
- If the string is genuinely the request body, drop the files= argument — you cannot mix a raw body with multipart files.
- 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
- Pass form fields as a dict, never as a URL-encoded string, when using files=.
- Do not mix raw-body (data=str) semantics with multipart (files=) semantics.
- Unit-test the upload path with both empty and non-empty field dicts.
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
- Files must be provided.
- Streamed bodies and files are mutually exclusive.
- Unsupported event specified, with event name
- You can only merge into CookieJar
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)