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
- Pass `data` as a dict of form fields: `requests.post(url, files=files, data={'meta': json.dumps(payload)})`.
- 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')}`.
- 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
- Never combine files= with a raw string body — multipart encoding needs key/value fields
- If you need a raw body plus a file, build the multipart body yourself (e.g. requests_toolbelt MultipartEncoder)
- Keep form data as a dict when uploading files
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
- Files must be provided.
- Invalid URL {url!r}: No scheme supplied. Perhaps you meant h
- URL has an invalid label.
- Streamed bodies and files are mutually exclusive.
- Header part ({header_part!r}) from {header} must be of type
AI-assisted analysis of psf/requests@414f0513c3 (2026-07-31).
Data as JSON: /data/errors/380a4c0c7ed31e8f.json.
Report an issue: GitHub ↗.