Skip to content

fix: handle bare dict and list annotations without type arguments - #3760

Open
rohitdeppi38 wants to merge 1 commit into
openai:mainfrom
rohitdeppi38:fix/bare-container-annotations
Open

fix: handle bare dict and list annotations without type arguments#3760
rohitdeppi38 wants to merge 1 commit into
openai:mainfrom
rohitdeppi38:fix/bare-container-annotations

Conversation

@rohitdeppi38

Copy link
Copy Markdown

Fixes #3338
Fixes #3341

The bug

transform() and construct_type() both assume that any dict or list annotation is parameterised, and index into get_args() unconditionally. For a bare, unparameterised annotation get_args() returns an empty tuple, so the index access raises instead of transforming/constructing the value.

from typing import TypedDict
from openai._utils._transform import transform
from openai._models import construct_type

class Params(TypedDict, total=False):
    metadata: dict  # bare dict — no type parameters

transform({"metadata": {"key": "value"}}, Params)
# IndexError: tuple index out of range          (_transform.py:183)

construct_type(value={"key": "value"}, type_=dict)
# ValueError: not enough values to unpack (expected 2, got 0)   (_models.py:660)

Two details that weren't in the linked issues:

  • Bare list is affected too. construct_type(value=[1, 2], type_=list) raises IndexError at args[0], and transform([1, 2], list) raises RuntimeError: Expected type <class 'list'> to have a type argument at index 0 via extract_type_arg. Same root cause, so it's fixed here as well.

  • This reaches real response deserialization. Any BaseModel with a bare dict/list field crashes on construction, because Model.construct() routes through the same construct_type() branch:

    class M(BaseModel):
        metadata: dict
    
    M.construct(metadata={"a": 1})
    # ValueError: not enough values to unpack (expected 2, got 0)

The async transform path (_async_transform_recursive) has the same two call sites and is fixed alongside the sync one.

The fix

Bare containers are treated as if their contents were annotated with Any, which is exactly the existing behaviour for dict[str, Any] / list[Any] — values pass through unchanged, while nested BaseModel values still get dumped for JSON-serializability. This keeps parameterised and unparameterised annotations consistent rather than special-casing bare ones into a "return unchanged" path.

  • _transform.py: new _extract_container_arg(typ, index) helper that falls back to object when the index is out of range, used by the dict and list/Iterable/Sequence branches in both the sync and async recursion.
  • _models.py: args is already computed at the top of construct_type, so the dict and list branches just guard the index access.

Verification

Added test_bare_dict_annotation / test_bare_list_annotation to tests/test_transform.py (parametrised over sync and async) and tests/test_models.py. All six new cases fail on main and pass with this change.

ruff check, ruff format --check, pyright, and mypy are clean on the four touched files. tests/test_models.py, tests/test_transform.py and tests/test_utils pass, with no change to the set of pre-existing failures.

`transform()` and `construct_type()` both assumed that any `dict` or `list`
annotation is parameterised, and indexed into `get_args()` unconditionally.
For a bare, unparameterised annotation `get_args()` returns an empty tuple,
so the index access raised instead of transforming/constructing the value:

```py
class Params(TypedDict, total=False):
    metadata: dict

transform({"metadata": {"key": "value"}}, Params)
# IndexError: tuple index out of range

construct_type(value={"key": "value"}, type_=dict)
# ValueError: not enough values to unpack (expected 2, got 0)
```

The same happened for bare `list` annotations, and in `construct_type()` this
also crashed for any `BaseModel` with a bare `dict`/`list` field, since
`Model.construct()` goes through the same code path.

Bare containers are now treated as if their contents were annotated with
`Any`, matching the existing behaviour of `dict[str, Any]` / `list[Any]`.

Fixes openai#3338
Fixes openai#3341
@rohitdeppi38
rohitdeppi38 requested a review from a team as a code owner August 29, 2026 13:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant