openai/openai-python · error · TypeError

Multipart extra_body must be a mapping

Error message

Multipart extra_body must be a mapping

What it means

When extra_body is supplied to a multipart request, it must be a Mapping so its keys can override/merge with the body fields. Passing a non-mapping extra_body (string, list, None-like objects that aren't None) makes the merge impossible and raises TypeError.

Source

Thrown at src/openai/_multipart.py:22

from typing import Mapping, cast

from ._types import Body, Omit, NotGiven, FileTypes, RequestFiles
from ._utils._json import openapi_dumps


def encode_multipart(
    body: object,
    extra_body: Body | None,
    encodings: Mapping[str, tuple[str, bool]],
    raw_body_field: str | None = None,
    existing_files: RequestFiles | None = None,
) -> tuple[dict[str, object] | None, RequestFiles | None, bytes | None, str]:
    """Prepare explicitly encoded form fields without flattening their JSON contents."""
    if not isinstance(body, Mapping):
        raise TypeError("Multipart request body must be a mapping")
    if extra_body is not None and not isinstance(extra_body, Mapping):
        raise TypeError("Multipart extra_body must be a mapping")
    original = cast(Mapping[str, object], body)
    overrides = cast(Mapping[str, object], extra_body or {})
    merged = {key: value for key, value in {**original, **overrides}.items() if not isinstance(value, (Omit, NotGiven))}
    # A raw request alternative is safe only when there is no other payload to lose.
    # Explicit null remains a JSON part; only an omitted field selects the raw body.
    if not existing_files and raw_body_field is not None and set(merged) == {raw_body_field}:
        value = merged[raw_body_field]
        if not isinstance(value, str):
            raise TypeError("Raw multipart alternative must be a string")
        return None, None, value.encode("utf-8"), encodings[raw_body_field][0]

    files: list[tuple[str, FileTypes]] = list(
        existing_files.items() if isinstance(existing_files, Mapping) else (existing_files or [])
    )
    for name, (content_type, as_json) in encodings.items():
        if name not in merged:
            continue
        value = merged.pop(name)

View on GitHub (pinned to 9917c6e28e)

Solutions

  1. Pass extra_body as a dict: extra_body={'key': 'value'}
  2. If extra_body comes from JSON text, json.loads() it first
  3. Omit extra_body entirely (pass None/NotGiven) when not needed

Example fix

// before
client.audio.transcriptions.create(..., extra_body='{"language": "en"}')
// after
client.audio.transcriptions.create(..., extra_body={"language": "en"})
Defensive patterns

Strategy: type-guard

Validate before calling

if extra_body is not None and not isinstance(extra_body, Mapping):
    extra_body = json.loads(extra_body)  # or reject

Type guard

from collections.abc import Mapping
from typing import TypeGuard

def is_valid_extra_body(value: object) -> TypeGuard[Mapping[str, object]]:
    return value is None or isinstance(value, Mapping)

Prevention

When it happens

Trigger: Calling a multipart endpoint with extra_body set to a JSON string, a list, or any non-dict value; programmatic construction of extra_body that defaults to a non-mapping.

Common situations: Developers reusing extra_body='{{"key": "value"}}' style serialized JSON from other tooling; forwarding a user-supplied parameter into extra_body without type checking.

Related errors


AI-assisted analysis of openai/openai-python@9917c6e28e (2026-08-28). Data as JSON: /api/errors/2013d8a91da319f8. Report an issue: GitHub.