Skip to content

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

Merged
marcuswood-oai merged 4 commits into
openai:mainfrom
rohitdeppi38:fix/bare-container-annotations
Sep 10, 2026
Merged

fix: handle bare dict and list annotations without type arguments#3760
marcuswood-oai merged 4 commits into
openai:mainfrom
rohitdeppi38:fix/bare-container-annotations

Conversation

@rohitdeppi38

Copy link
Copy Markdown
Contributor

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
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 9, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-09T23:57:45.695091Z a91b398 New commits
🔒 Security Review Completed 2026-09-09T23:58:30.527886Z a91b398 New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

pull Bot pushed a commit to Superoldman96/openai-python that referenced this pull request Sep 9, 2026
## Summary

Fork PRs openai#3760 and openai#3345 have green Python CI but no required Castiron
budget statuses: both the workflow-run association and commit-to-PR
endpoint return no PRs. The trusted evaluator fails with `source run
must identify exactly one current PR targeting main`, and the publisher
returns without posting statuses.

Fall back to listing open PRs by the source run's fork owner and branch,
then retain the existing exact-head, target, freshness, and ambiguity
checks. Apply the fallback to report computation, budget evaluation, and
publication; update the reporter integrity digest.

## Validation

- Regression tests reproduce the evaluator failure and missing statuses
on unpatched main.
- Patched read-only lookup resolves the exact current heads of both
affected PRs.
- Castiron reporter/budget suite: 53 tests, OK (1 optional
compiler-contract test skipped), including the JavaScript publishers,
pagination, stale heads, wrong targets, and ambiguous matches.
- Ruff, Pyright, and mypy for `scripts/castiron`.

The trusted handler runs from main. After this fix merges, rerun the
Castiron custom-code workflows for openai#3760 and openai#3345 to publish their
required statuses.
@github-actions

github-actions Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Castiron custom code

✅ No new custom-code files detected.

36 mixed files remain; 0 existing customizations changed.

Compared 397ea08d8cf1a91b398382e8. Generated baselines verified.

36 existing customizations unchanged
  • api.md
  • scripts/castiron/README.md
  • scripts/castiron/custom_code_report.py
  • scripts/castiron/test_custom_code_report.py
  • src/openai/init.py
  • src/openai/_client.py
  • src/openai/resources/audio/transcriptions.py
  • src/openai/resources/audio/translations.py
  • src/openai/resources/beta/beta.py
  • src/openai/resources/beta/responses/responses.py
  • src/openai/resources/beta/threads/runs/runs.py
  • src/openai/resources/beta/threads/threads.py
  • src/openai/resources/chat/completions/completions.py
  • src/openai/resources/embeddings.py
  • src/openai/resources/files.py
  • src/openai/resources/realtime/realtime.py
  • src/openai/resources/responses/responses.py
  • src/openai/resources/uploads/uploads.py
  • src/openai/resources/vector_stores/file_batches.py
  • src/openai/resources/vector_stores/files.py
  • src/openai/resources/videos.py
  • src/openai/resources/webhooks/init.py
  • src/openai/resources/webhooks/webhooks.py
  • src/openai/types/chat/init.py
  • src/openai/types/chat/chat_completion_message_tool_call.py
  • src/openai/types/fine_tuning/fine_tuning_job_integration.py
  • src/openai/types/responses/init.py
  • src/openai/types/responses/response.py
  • src/openai/types/responses/response_function_web_search.py
  • src/openai/types/responses/response_function_web_search_param.py
  • src/openai/types/responses/responses_client_event.py
  • src/openai/types/responses/responses_client_event_param.py
  • src/openai/types/responses/tool.py
  • src/openai/types/responses/tool_param.py
  • src/openai/types/webhooks/init.py
  • tests/api_resources/test_videos.py

A changed generated baseline means this report cannot reliably identify which handwritten lines changed.

Inspect the custom-code diff

Download the exact patch produced by this run (requires repository access):

gh run download 34419103051 --repo openai/openai-python \
  --name castiron-custom-code-34419103051-1 --dir /tmp/castiron-custom-code-34419103051-1
git apply --stat /tmp/castiron-custom-code-34419103051-1/custom-code.patch
cat /tmp/castiron-custom-code-34419103051-1/custom-code.patch

Or reproduce it from an SDK checkout containing the vendored reporter:

git fetch --no-tags origin 397ea08d8cf151039c069c1173f5de57cff5c081 a91b398382e8ad81af3e288f78334e0eb9bae6ab
python3 scripts/castiron/custom_code_report.py report \
  --base 397ea08d8cf151039c069c1173f5de57cff5c081 \
  --head a91b398382e8ad81af3e288f78334e0eb9bae6ab --fetch --require-head-hash --public \
  --out /tmp/castiron-custom-code-a91b398382e8
cat /tmp/castiron-custom-code-a91b398382e8/custom-code.patch

This is the current full custom patch for mixed files, not an attribution of only the handwritten lines changed by this PR.

Full report and patch

pull Bot pushed a commit to Pyrex77/openai-python that referenced this pull request Sep 10, 2026
Automated Release PR
---


##
[3.12.0](openai/openai-python@v3.11.0...v3.12.0)
(2026-09-10)


### Features

* **api:** Add Live API
([0e4bfef](openai@0e4bfef))


### Bug Fixes

* add aclose() to AsyncStream for standard async cleanup
([openai#2854](openai#2854))
([802b334](openai@802b334))
* handle bare `dict` and `list` annotations without type arguments
([openai#3760](openai#3760))
([c7e8c03](openai@c7e8c03))
* preserve finalized output on null response completion
([openai#3345](openai#3345))
([adb212e](openai@adb212e))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

Co-authored-by: openai-sdks[bot] <284451331+openai-sdks[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants