fix: handle bare dict and list annotations without type arguments - #3760
Conversation
`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
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
## 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.
Castiron custom code✅ No new custom-code files detected. 36 mixed files remain; 0 existing customizations changed. Compared 36 existing customizations unchanged
A changed generated baseline means this report cannot reliably identify which handwritten lines changed. Inspect the custom-code diffDownload 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.patchOr 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.patchThis is the current full custom patch for mixed files, not an attribution of only the handwritten lines changed by this PR. |
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>
Fixes #3338
Fixes #3341
The bug
transform()andconstruct_type()both assume that anydictorlistannotation is parameterised, and index intoget_args()unconditionally. For a bare, unparameterised annotationget_args()returns an empty tuple, so the index access raises instead of transforming/constructing the value.Two details that weren't in the linked issues:
Bare
listis affected too.construct_type(value=[1, 2], type_=list)raisesIndexErroratargs[0], andtransform([1, 2], list)raisesRuntimeError: Expected type <class 'list'> to have a type argument at index 0viaextract_type_arg. Same root cause, so it's fixed here as well.This reaches real response deserialization. Any
BaseModelwith a baredict/listfield crashes on construction, becauseModel.construct()routes through the sameconstruct_type()branch: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 fordict[str, Any]/list[Any]— values pass through unchanged, while nestedBaseModelvalues 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 toobjectwhen the index is out of range, used by thedictandlist/Iterable/Sequencebranches in both the sync and async recursion._models.py:argsis already computed at the top ofconstruct_type, so thedictandlistbranches just guard the index access.Verification
Added
test_bare_dict_annotation/test_bare_list_annotationtotests/test_transform.py(parametrised over sync and async) andtests/test_models.py. All six new cases fail onmainand pass with this change.ruff check,ruff format --check,pyright, andmypyare clean on the four touched files.tests/test_models.py,tests/test_transform.pyandtests/test_utilspass, with no change to the set of pre-existing failures.