aio-libs/aiohttp · error · TypeError

expected str, got {value!r}

Error message

expected str, got {value!r}

What it means

Raised by FormData._gen_form_urlencoded when, in urlencoded (non-multipart) mode, a field value is not a str. urlencoded bodies can only carry string key/value pairs, so int/None/bytes values are rejected at serialization time with a TypeError. This path runs only when is_multipart is False.

Source

Thrown at aiohttp/formdata.py:112

                to_add.extend(rec.items())

            elif isinstance(rec, (list, tuple)) and len(rec) == 2:
                k, fp = rec
                self.add_field(k, fp)

            else:
                raise TypeError(
                    "Only io.IOBase, multidict and (name, file) "
                    "pairs allowed, use .add_field() for passing "
                    f"more complex parameters, got {rec!r}"
                )

    def _gen_form_urlencoded(self) -> payload.BytesPayload:
        # form data (x-www-form-urlencoded)
        data = []
        for type_options, _, value in self._fields:
            if not isinstance(value, str):
                raise TypeError(f"expected str, got {value!r}")
            data.append((type_options["name"], value))

        charset = self._charset if self._charset is not None else "utf-8"

        if charset == "utf-8":
            content_type = "application/x-www-form-urlencoded"
        else:
            content_type = "application/x-www-form-urlencoded; charset=%s" % charset

        return payload.BytesPayload(
            urlencode(data, doseq=True, encoding=charset).encode(),
            content_type=content_type,
        )

    def _gen_form_data(self) -> multipart.MultipartWriter:
        """Encode a list of fields using the multipart/form-data MIME format"""
        for dispparams, headers, value in self._fields:
            try:

View on GitHub (pinned to c0ef574e29)

Solutions

  1. Stringify every value: FormData({k: str(v) for k, v in data.items()}).
  2. Convert None to '' explicitly.
  3. Force multipart if you need binary: pass default_to_multipart=True or include a bytes value.

Example fix

// before
form = FormData({'count': 5, 'name': None})
// after
form = FormData({'count': str(5), 'name': ''})
Defensive patterns

Strategy: validation

Validate before calling

def stringify_form(data: dict) -> dict:
    return {k: ('' if v is None else str(v)) for k, v in data.items()}

Type guard

def all_str_values(data) -> bool:
    return all(isinstance(v, str) for v in data.values())

Prevention

When it happens

Trigger: FormData({'count': 5}) or FormData([('name', None)]) where no field forced multipart mode (no file/bytes/content_type). Calling form() then triggers _gen_form_urlencoded which iterates fields and hits the non-str value.

Common situations: Forgetting to stringify ints/floats/bools from form data; passing None for empty fields instead of ''; expecting requests-like coercion of non-string values.

Related errors


AI-assisted analysis of aio-libs/aiohttp@c0ef574e29 (2026-08-04). Data as JSON: /data/errors/f78b10ef8a0e6cf7.json. Report an issue: GitHub.