Skip to content

[ty] Support Unpack[TypedDict] in **kwargs signatures#24653

Open
charliermarsh wants to merge 3 commits intomainfrom
charlie/unpack
Open

[ty] Support Unpack[TypedDict] in **kwargs signatures#24653
charliermarsh wants to merge 3 commits intomainfrom
charlie/unpack

Conversation

@charliermarsh
Copy link
Copy Markdown
Member

@charliermarsh charliermarsh commented Apr 15, 2026

Summary

We now support Unpack[TypedDict] as an annotation on **kwargs, as in the following example:

from typing_extensions import TypedDict, Unpack

class MovieKwargs(TypedDict):
    title: str
    year: int

def show_movie(**kwargs: Unpack[MovieKwargs]) -> None:
    ...

show_movie(title="Alien", year=1979)  # OK
show_movie(title="Alien")             # missing required key
show_movie(name="Alien", year=1979)   # unknown keyword

@astral-sh-bot astral-sh-bot Bot added the ty Multi-file analysis & type inference label Apr 15, 2026
@astral-sh-bot
Copy link
Copy Markdown

astral-sh-bot Bot commented Apr 15, 2026

Typing conformance results improved 🎉

The percentage of diagnostics emitted that were expected errors increased from 87.94% to 88.34%. The percentage of expected errors that received a diagnostic increased from 83.36% to 84.42%. The number of fully passing files improved from 79/133 to 81/133.

Summary

How are test cases classified?

Each test case represents one expected error annotation or a group of annotations sharing a tag. Counts are per test case, not per diagnostic — multiple diagnostics on the same line count as one. Required annotations (E) are true positives when ty flags the expected location and false negatives when it does not. Optional annotations (E?) are true positives when flagged but true negatives (not false negatives) when not. Tagged annotations (E[tag]) require ty to flag exactly one of the tagged lines; tagged multi-annotations (E[tag+]) allow any number up to the tag count. Flagging unexpected locations counts as a false positive.

Metric Old New Diff Outcome
True Positives 882 894 +12 ⏫ (✅)
False Positives 121 118 -3 ⏬ (✅)
False Negatives 176 165 -11 ⏬ (✅)
Total Diagnostics 1052 1062 +10
Precision 87.94% 88.34% +0.40% ⏫ (✅)
Recall 83.36% 84.42% +1.05% ⏫ (✅)
Passing Files 79/133 81/133 +2 ⏫ (✅)

Test file breakdown

3 files altered
File True Positives False Positives False Negatives Status
callables_kwargs.py 14 (+10) ✅ 0 (-4) ✅ 0 (-9) ✅ ✅ Newly Passing 🎉
typeddicts_extra_items.py 12 (+1) ✅ 23 (+1) ❌ 16 (-1) ✅ ➡️ Neutral
typeddicts_readonly_kwargs.py 1 (+1) ✅ 0 0 (-1) ✅ ✅ Newly Passing 🎉
Total (all files) 894 (+12) ✅ 118 (-3) ✅ 165 (-11) ✅ 81/133

True positives added (11)

11 diagnostics
Test case Diff

callables_kwargs.py:101

+error[invalid-assignment] Object of type `def func1(*, v1: int, v2: str = ..., v3: str) -> None` is not assignable to `TDProtocol3`

callables_kwargs.py:102

+error[invalid-assignment] Object of type `def func1(*, v1: int, v2: str = ..., v3: str) -> None` is not assignable to `TDProtocol4`

callables_kwargs.py:111

+error[invalid-type-form] Parameter `v1` overlaps with unpacked TypedDict key in `**kwargs` annotation

callables_kwargs.py:122

+error[invalid-type-form] Unpacked value for `**kwargs` must be a TypedDict, not `T@func6`

callables_kwargs.py:46

+error[missing-argument] No arguments provided for required parameters `v1`, `v3` of function `func1`

callables_kwargs.py:51

+error[unknown-argument] Argument `v4` does not match any known parameter of function `func1`

callables_kwargs.py:58

+error[invalid-argument-type] Argument to function `func1` is incorrect: Expected `int`, found `str`

callables_kwargs.py:63

+error[parameter-already-assigned] Multiple values provided for parameter `v1` of function `func1`

callables_kwargs.py:65

+error[parameter-already-assigned] Multiple values provided for parameter `v1` of function `func2`

typeddicts_extra_items.py:143

+error[unknown-argument] Argument `year` does not match any known parameter of function `unpack_no_extra`

typeddicts_readonly_kwargs.py:33

+error[invalid-assignment] Cannot assign to key "key1" on TypedDict `ReadOnlyArgs`: key is marked read-only

False positives removed (4)

4 diagnostics
Test case Diff

callables_kwargs.py:24

-error[type-assertion-failure] Type `@Todo(`Unpack[]` special form)` does not match asserted type `int`

callables_kwargs.py:32

-error[type-assertion-failure] Type `@Todo(`Unpack[]` special form)` does not match asserted type `str`

callables_kwargs.py:35

-error[type-assertion-failure] Type `@Todo(`Unpack[]` special form)` does not match asserted type `str`

callables_kwargs.py:41

-error[type-assertion-failure] Type `dict[str, @Todo(`Unpack[]` special form)]` does not match asserted type `TD1`

True positives changed (2)

2 diagnostics
Test case Diff

callables_kwargs.py:103

-error[invalid-assignment] Object of type `def func1(**kwargs: @Todo(`Unpack[]` special form)) -> None` is not assignable to `TDProtocol5`
+error[invalid-assignment] Object of type `def func1(*, v1: int, v2: str = ..., v3: str) -> None` is not assignable to `TDProtocol5`

callables_kwargs.py:52

-error[too-many-positional-arguments] Too many positional arguments to function `func1`: expected 0, got 3
+error[missing-argument] No arguments provided for required parameters `v1`, `v3` of function `func1`
+error[too-many-positional-arguments] Too many positional arguments to function `func1`: expected 0, got 3

False positives added (1)

1 diagnostic
Test case Diff

typeddicts_extra_items.py:144

+error[unknown-argument] Argument `year` does not match any known parameter of function `unpack_extra`

Optional Diagnostics Added (1)

1 diagnostic
Test case Diff

callables_kwargs.py:61

+error[invalid-argument-type] Argument to function `func1` is incorrect: Expected `str`, found `int | str`

@astral-sh-bot
Copy link
Copy Markdown

astral-sh-bot Bot commented Apr 15, 2026

Memory usage report

Summary

Project Old New Diff Outcome
prefect 704.14MB 711.29MB +1.02% (7.15MB)
sphinx 258.60MB 261.94MB +1.29% (3.34MB)
trio 116.46MB 117.87MB +1.21% (1.41MB)
flake8 47.68MB 48.05MB +0.78% (380.84kB)

Significant changes

Click to expand detailed breakdown

prefect

Name Old New Diff Outcome
FunctionType 8.95MB 11.65MB +30.21% (2.70MB)
FunctionType<'db>::last_definition_raw_signature_ 3.46MB 5.31MB +53.40% (1.85MB)
FunctionType<'db>::signature_ 4.11MB 5.49MB +33.82% (1.39MB)
CallableType 2.15MB 2.81MB +30.61% (674.29kB)
FunctionType<'db>::last_definition_signature_ 818.90kB 1.13MB +41.27% (337.94kB)
infer_expression_types_impl 62.86MB 62.90MB +0.06% (38.80kB)
infer_definition_types 90.12MB 90.15MB +0.03% (25.73kB)
infer_expression_type_impl 13.41MB 13.43MB +0.18% (24.44kB)
Type<'db>::apply_specialization_ 3.69MB 3.71MB +0.53% (20.00kB)
StaticClassLiteral<'db>::implicit_attribute_inner_ 10.07MB 10.09MB +0.16% (16.08kB)
UnionType 939.09kB 950.22kB +1.18% (11.12kB)
Type<'db>::member_lookup_with_policy_ 17.28MB 17.29MB +0.05% (9.56kB)
Type<'db>::try_call_dunder_get_ 10.60MB 10.61MB +0.08% (9.19kB)
GenericAlias<'db>::variance_of_ 579.59kB 587.26kB +1.32% (7.68kB)
all_narrowing_constraints_for_expression 7.22MB 7.22MB +0.10% (7.54kB)
... 43 more

sphinx

Name Old New Diff Outcome
FunctionType<'db>::last_definition_raw_signature_ 2.04MB 3.22MB +57.61% (1.18MB)
FunctionType 3.11MB 4.01MB +29.04% (923.42kB)
FunctionType<'db>::signature_ 2.27MB 3.17MB +39.53% (919.30kB)
CallableType 1.12MB 1.38MB +23.75% (271.19kB)
FunctionType<'db>::last_definition_signature_ 221.45kB 319.92kB +44.46% (98.47kB)

trio

Name Old New Diff Outcome
FunctionType 1.48MB 1.90MB +27.73% (421.45kB)
FunctionType<'db>::signature_ 1.06MB 1.47MB +38.29% (416.01kB)
FunctionType<'db>::last_definition_raw_signature_ 601.46kB 964.96kB +60.44% (363.50kB)
CallableType 576.09kB 709.46kB +23.15% (133.37kB)
FunctionType<'db>::last_definition_signature_ 242.30kB 348.65kB +43.89% (106.34kB)
infer_definition_types 7.55MB 7.56MB +0.02% (1.23kB)
StaticClassLiteral<'db>::own_fields_ 7.35kB 8.46kB +15.15% (1.11kB)
StaticClassLiteral<'db>::fields_inner_ 12.88kB 13.92kB +8.10% (1.04kB)
class_based_items 3.62kB 3.91kB +8.21% (304.00B)
code_generator_of_static_class 307.79kB 308.07kB +0.09% (284.00B)
place_by_id 549.41kB 549.65kB +0.04% (248.00B)
Type<'db>::apply_specialization_::interned_arguments 624.53kB 624.77kB +0.04% (240.00B)
StaticClassLiteral<'db>::try_metaclass_ 139.05kB 139.28kB +0.16% (232.00B)
StaticClassLiteral<'db>::try_mro_ 790.26kB 790.46kB +0.03% (212.00B)
Type<'db>::apply_specialization_ 704.68kB 704.84kB +0.02% (168.00B)
... 10 more

flake8

Name Old New Diff Outcome
FunctionType<'db>::signature_ 360.42kB 499.11kB +38.48% (138.69kB)
FunctionType 437.97kB 557.66kB +27.33% (119.69kB)
FunctionType<'db>::last_definition_raw_signature_ 80.73kB 132.61kB +64.27% (51.88kB)
CallableType 168.82kB 213.58kB +26.51% (44.76kB)
FunctionType<'db>::last_definition_signature_ 59.28kB 85.10kB +43.56% (25.82kB)

@astral-sh-bot
Copy link
Copy Markdown

astral-sh-bot Bot commented Apr 15, 2026

ecosystem-analyzer results

Lint rule Added Removed Changed
invalid-argument-type 90 1 5
unused-type-ignore-comment 0 36 0
unknown-argument 31 0 0
invalid-type-form 11 0 0
no-matching-overload 8 0 0
invalid-method-override 3 0 0
not-subscriptable 0 3 0
invalid-return-type 1 1 0
unresolved-attribute 0 2 0
invalid-assignment 0 1 0
type-assertion-failure 0 1 0
Total 144 45 5
Raw diff (194 changes)
aiohttp-devtools (https://github.com/aio-libs/aiohttp-devtools)
+ aiohttp_devtools/runserver/watch.py:120:55 error[invalid-argument-type] Argument to bound method `ClientSession.get` is incorrect: Expected `SSLContext | bool | Fingerprint`, found `None | SSLContext`
+ aiohttp_devtools/runserver/watch.py:155:57 error[invalid-argument-type] Argument to bound method `ClientSession.get` is incorrect: Expected `SSLContext | bool | Fingerprint`, found `None | SSLContext`

altair (https://github.com/vega/altair)
- altair/datasets/_constraints.py:54:24 error[invalid-return-type] Return type does not match returned value: expected `Metadata`, found `dict[str, @Todo]`
- altair/datasets/_constraints.py:108:33 error[invalid-argument-type] Argument to bound method `MetaIs.from_metadata` is incorrect: Expected `Metadata`, found `dict[str, @Todo]`
- altair/utils/core.py:680:18 error[unresolved-attribute] Attribute `schema` is not defined on `NativeDataFrame`, `DataFrameLike`, `None` in union `NativeDataFrame | DataFrameLike | Unknown | None`
- altair/utils/core.py:682:22 error[not-subscriptable] Cannot subscript object of type `DataFrameLike` with no `__getitem__` method
- altair/utils/core.py:682:22 error[not-subscriptable] Cannot subscript object of type `NativeDataFrame` with no `__getitem__` method
- altair/utils/core.py:682:22 error[not-subscriptable] Cannot subscript object of type `None` with no `__getitem__` method
- altair/utils/core.py:686:39 error[unresolved-attribute] Attribute `to_native` is not defined on `NativeDataFrame`, `DataFrameLike`, `None` in union `NativeDataFrame | DataFrameLike | Unknown | None`

bokeh (https://github.com/bokeh/bokeh)
+ src/bokeh/plotting/_graph.py:131:9 error[invalid-argument-type] Argument to `GlyphRenderer.__init__` is incorrect: Argument type `Glyph | None` does not satisfy upper bound `Glyph` of type variable `GlyphType`
+ src/bokeh/plotting/_graph.py:131:9 error[invalid-argument-type] Argument to `GlyphRenderer.__init__` is incorrect: Expected `Glyph`, found `Glyph | None`
+ src/bokeh/plotting/_graph.py:149:9 error[invalid-argument-type] Argument to `GlyphRenderer.__init__` is incorrect: Argument type `Glyph | None` does not satisfy upper bound `Glyph` of type variable `GlyphType`
+ src/bokeh/plotting/_graph.py:149:9 error[invalid-argument-type] Argument to `GlyphRenderer.__init__` is incorrect: Expected `Glyph`, found `Glyph | None`
+ src/bokeh/transform.py:155:13 error[invalid-argument-type] Argument to `EqHistColorMapper.__init__` is incorrect: Expected `Sequence[str | tuple[int, int, int] | tuple[int, int, int, int | float]]`, found `Sequence[str | Color | tuple[int, int, int] | tuple[int, int, int, int | float]]`
+ src/bokeh/transform.py:158:13 error[invalid-argument-type] Argument to `EqHistColorMapper.__init__` is incorrect: Expected `str | tuple[int, int, int] | tuple[int, int, int, int | float]`, found `str | Color | tuple[int, int, int] | tuple[int, int, int, int | float]`
+ src/bokeh/transform.py:159:13 error[invalid-argument-type] Argument to `EqHistColorMapper.__init__` is incorrect: Expected `str | tuple[int, int, int] | tuple[int, int, int, int | float] | None`, found `str | Color | tuple[int, int, int] | tuple[int, int, int, int | float] | None`
+ src/bokeh/transform.py:160:13 error[invalid-argument-type] Argument to `EqHistColorMapper.__init__` is incorrect: Expected `str | tuple[int, int, int] | tuple[int, int, int, int | float] | None`, found `str | Color | tuple[int, int, int] | tuple[int, int, int, int | float] | None`
+ src/bokeh/transform.py:200:13 error[invalid-argument-type] Argument to `CategoricalColorMapper.__init__` is incorrect: Expected `Sequence[str | tuple[int, int, int] | tuple[int, int, int, int | float]]`, found `Sequence[str | Color | tuple[int, int, int] | tuple[int, int, int, int | float]]`
+ src/bokeh/transform.py:202:13 error[invalid-argument-type] Argument to `CategoricalColorMapper.__init__` is incorrect: Expected `int`, found `int | float`
+ src/bokeh/transform.py:203:13 error[invalid-argument-type] Argument to `CategoricalColorMapper.__init__` is incorrect: Expected `int | None`, found `int | float | None`
+ src/bokeh/transform.py:204:13 error[invalid-argument-type] Argument to `CategoricalColorMapper.__init__` is incorrect: Expected `str | tuple[int, int, int] | tuple[int, int, int, int | float]`, found `str | Color | tuple[int, int, int] | tuple[int, int, int, int | float]`
+ src/bokeh/transform.py:242:13 error[invalid-argument-type] Argument to `CategoricalPatternMapper.__init__` is incorrect: Expected `Sequence[Property[Literal["blank", "dot", "ring", "horizontal_line", "vertical_line", ... omitted 29 literals]]]`, found `Sequence[str]`
+ src/bokeh/transform.py:244:13 error[invalid-argument-type] Argument to `CategoricalPatternMapper.__init__` is incorrect: Expected `int`, found `int | float`
+ src/bokeh/transform.py:245:13 error[invalid-argument-type] Argument to `CategoricalPatternMapper.__init__` is incorrect: Expected `int | None`, found `int | float | None`
+ src/bokeh/transform.py:285:13 error[invalid-argument-type] Argument to `CategoricalMarkerMapper.__init__` is incorrect: Expected `Sequence[Literal["asterisk", "circle", "circle_cross", "circle_dot", "circle_x", ... omitted 23 literals]]`, found `Sequence[str]`
+ src/bokeh/transform.py:287:13 error[invalid-argument-type] Argument to `CategoricalMarkerMapper.__init__` is incorrect: Expected `int`, found `int | float`
+ src/bokeh/transform.py:288:13 error[invalid-argument-type] Argument to `CategoricalMarkerMapper.__init__` is incorrect: Expected `int | None`, found `int | float | None`
+ src/bokeh/transform.py:368:13 error[invalid-argument-type] Argument to `LinearColorMapper.__init__` is incorrect: Expected `Sequence[str | tuple[int, int, int] | tuple[int, int, int, int | float]]`, found `Sequence[str | Color | tuple[int, int, int] | tuple[int, int, int, int | float]]`
+ src/bokeh/transform.py:371:13 error[invalid-argument-type] Argument to `LinearColorMapper.__init__` is incorrect: Expected `str | tuple[int, int, int] | tuple[int, int, int, int | float]`, found `str | Color | tuple[int, int, int] | tuple[int, int, int, int | float]`
+ src/bokeh/transform.py:372:13 error[invalid-argument-type] Argument to `LinearColorMapper.__init__` is incorrect: Expected `str | tuple[int, int, int] | tuple[int, int, int, int | float] | None`, found `str | Color | tuple[int, int, int] | tuple[int, int, int, int | float] | None`
+ src/bokeh/transform.py:373:13 error[invalid-argument-type] Argument to `LinearColorMapper.__init__` is incorrect: Expected `str | tuple[int, int, int] | tuple[int, int, int, int | float] | None`, found `str | Color | tuple[int, int, int] | tuple[int, int, int, int | float] | None`
+ src/bokeh/transform.py:415:13 error[invalid-argument-type] Argument to `LogColorMapper.__init__` is incorrect: Expected `Sequence[str | tuple[int, int, int] | tuple[int, int, int, int | float]]`, found `Sequence[str | Color | tuple[int, int, int] | tuple[int, int, int, int | float]]`
+ src/bokeh/transform.py:418:13 error[invalid-argument-type] Argument to `LogColorMapper.__init__` is incorrect: Expected `str | tuple[int, int, int] | tuple[int, int, int, int | float]`, found `str | Color | tuple[int, int, int] | tuple[int, int, int, int | float]`
+ src/bokeh/transform.py:419:13 error[invalid-argument-type] Argument to `LogColorMapper.__init__` is incorrect: Expected `str | tuple[int, int, int] | tuple[int, int, int, int | float] | None`, found `str | Color | tuple[int, int, int] | tuple[int, int, int, int | float] | None`
+ src/bokeh/transform.py:420:13 error[invalid-argument-type] Argument to `LogColorMapper.__init__` is incorrect: Expected `str | tuple[int, int, int] | tuple[int, int, int, int | float] | None`, found `str | Color | tuple[int, int, int] | tuple[int, int, int, int | float] | None`
+ src/bokeh/layouts.py:384:9 error[invalid-argument-type] Argument to `Toolbar.__init__` is incorrect: Expected `Literal["normal", "grey"] | None`, found `Literal["normal", "grey"] | None | UndefinedType`
+ src/bokeh/layouts.py:385:9 error[invalid-argument-type] Argument to `Toolbar.__init__` is incorrect: Expected `bool`, found `bool | UndefinedType`
+ src/bokeh/layouts.py:386:9 error[invalid-argument-type] Argument to `Toolbar.__init__` is incorrect: Expected `Literal["auto"] | Drag | ToolProxy | None`, found `ToolProxy | Tool | UndefinedType`
+ src/bokeh/layouts.py:387:9 error[invalid-argument-type] Argument to `Toolbar.__init__` is incorrect: Expected `Literal["auto"] | InspectTool | ToolProxy | Sequence[InspectTool] | None`, found `ToolProxy | Tool | UndefinedType`
+ src/bokeh/layouts.py:388:9 error[invalid-argument-type] Argument to `Toolbar.__init__` is incorrect: Expected `Literal["auto"] | Scroll | ToolProxy | None`, found `ToolProxy | Tool | UndefinedType`
+ src/bokeh/layouts.py:389:9 error[invalid-argument-type] Argument to `Toolbar.__init__` is incorrect: Expected `Literal["auto"] | Tap | ToolProxy | None`, found `ToolProxy | Tool | UndefinedType`
+ src/bokeh/layouts.py:390:9 error[invalid-argument-type] Argument to `Toolbar.__init__` is incorrect: Expected `Literal["auto"] | GestureTool | ToolProxy | None`, found `ToolProxy | Tool | UndefinedType`
+ src/bokeh/layouts.py:394:9 error[unknown-argument] Argument `children` does not match any known parameter of `GridPlot.__init__`
+ src/bokeh/models/renderers/contour_renderer.py:112:13 error[invalid-argument-type] Argument to `ContourColorBar.__init__` is incorrect: Expected `GlyphRenderer[Glyph]`, found `Instance[GlyphRenderer[Unknown]]`
+ src/bokeh/models/renderers/contour_renderer.py:113:13 error[invalid-argument-type] Argument to `ContourColorBar.__init__` is incorrect: Expected `GlyphRenderer[Glyph]`, found `Instance[GlyphRenderer[Unknown]]`
+ src/bokeh/models/renderers/contour_renderer.py:114:13 error[invalid-argument-type] Argument to `ContourColorBar.__init__` is incorrect: Expected `Sequence[int | float]`, found `Seq[T@Seq]`
+ src/bokeh/models/renderers/contour_renderer.py:115:32 error[invalid-argument-type] Argument to `FixedTicker.__init__` is incorrect: Expected `Sequence[int | float]`, found `Seq[T@Seq]`
- src/bokeh/models/renderers/graph_renderer.py:84:45 error[invalid-argument-type] Argument to `Object.__init__` is incorrect: Argument type `() -> GlyphRenderer[Unknown]` does not satisfy upper bound `Serializable` of type variable `S`
+ src/bokeh/models/renderers/graph_renderer.py:84:45 error[invalid-argument-type] Argument to `Object.__init__` is incorrect: Argument type `() -> GlyphRenderer[Scatter]` does not satisfy upper bound `Serializable` of type variable `S`
- src/bokeh/models/renderers/graph_renderer.py:84:45 error[invalid-argument-type] Argument to `Object.__init__` is incorrect: Expected `GlyphRenderer[Unknown] | UndefinedType | IntrinsicType`, found `() -> GlyphRenderer[Unknown]`
+ src/bokeh/models/renderers/graph_renderer.py:84:45 error[invalid-argument-type] Argument to `Object.__init__` is incorrect: Expected `GlyphRenderer[Unknown] | UndefinedType | IntrinsicType`, found `() -> GlyphRenderer[Scatter]`
- src/bokeh/models/renderers/graph_renderer.py:89:45 error[invalid-argument-type] Argument to `Object.__init__` is incorrect: Argument type `() -> GlyphRenderer[Unknown]` does not satisfy upper bound `Serializable` of type variable `S`
+ src/bokeh/models/renderers/graph_renderer.py:89:45 error[invalid-argument-type] Argument to `Object.__init__` is incorrect: Argument type `() -> GlyphRenderer[MultiLine]` does not satisfy upper bound `Serializable` of type variable `S`
- src/bokeh/models/renderers/graph_renderer.py:89:45 error[invalid-argument-type] Argument to `Object.__init__` is incorrect: Expected `GlyphRenderer[Unknown] | UndefinedType | IntrinsicType`, found `() -> GlyphRenderer[Unknown]`
+ src/bokeh/models/renderers/graph_renderer.py:89:45 error[invalid-argument-type] Argument to `Object.__init__` is incorrect: Expected `GlyphRenderer[Unknown] | UndefinedType | IntrinsicType`, found `() -> GlyphRenderer[MultiLine]`
+ src/bokeh/plotting/_figure.py:243:41 error[invalid-argument-type] Argument to `CoordinateMapping.__init__` is incorrect: Expected `Range`, found `Range | None`
+ src/bokeh/plotting/_figure.py:243:60 error[invalid-argument-type] Argument to `CoordinateMapping.__init__` is incorrect: Expected `Range`, found `Range | None`
+ src/bokeh/plotting/_geo_feature.pyi:24:24 error[invalid-type-form] Unpacked value for `**kwargs` must be a TypedDict, not `Unknown`
+ src/bokeh/plotting/_geo_feature.pyi:31:24 error[invalid-type-form] Unpacked value for `**kwargs` must be a TypedDict, not `Unknown`
+ src/bokeh/plotting/_geo_feature.pyi:38:24 error[invalid-type-form] Unpacked value for `**kwargs` must be a TypedDict, not `Unknown`
+ src/bokeh/plotting/_geo_feature.pyi:45:24 error[invalid-type-form] Unpacked value for `**kwargs` must be a TypedDict, not `Unknown`
+ src/bokeh/plotting/_geo_feature.pyi:52:24 error[invalid-type-form] Unpacked value for `**kwargs` must be a TypedDict, not `Unknown`
+ src/bokeh/plotting/_geo_feature.pyi:59:24 error[invalid-type-form] Unpacked value for `**kwargs` must be a TypedDict, not `Unknown`
+ src/bokeh/plotting/_geo_feature.pyi:66:24 error[invalid-type-form] Unpacked value for `**kwargs` must be a TypedDict, not `Unknown`
+ src/bokeh/plotting/_geo_feature.pyi:73:24 error[invalid-type-form] Unpacked value for `**kwargs` must be a TypedDict, not `Unknown`
+ src/bokeh/plotting/_geo_feature.pyi:79:24 error[invalid-type-form] Unpacked value for `**kwargs` must be a TypedDict, not `Unknown`
+ src/bokeh/plotting/_geo_feature.pyi:86:24 error[invalid-type-form] Unpacked value for `**kwargs` must be a TypedDict, not `Unknown`
+ src/bokeh/plotting/_geo_feature.pyi:93:24 error[invalid-type-form] Unpacked value for `**kwargs` must be a TypedDict, not `Unknown`
+ src/bokeh/plotting/_plot.py:98:32 error[invalid-argument-type] Argument to `Range1d.__init__` is incorrect: Expected `int | float | datetime | timedelta`, found `int | float | (Unknown & ~None) | str | IntrinsicType`
+ src/bokeh/plotting/_plot.py:98:45 error[invalid-argument-type] Argument to `Range1d.__init__` is incorrect: Expected `int | float | datetime | timedelta`, found `int | float | (Unknown & ~None) | str | IntrinsicType`
+ src/bokeh/plotting/_renderer.py:127:9 error[invalid-argument-type] Argument to `GlyphRenderer.__init__` is incorrect: Argument type `Glyph | None` does not satisfy upper bound `Glyph` of type variable `GlyphType`
+ src/bokeh/plotting/_renderer.py:127:9 error[invalid-argument-type] Argument to `GlyphRenderer.__init__` is incorrect: Expected `Glyph`, found `Glyph | None`
+ src/bokeh/plotting/_renderer.py:139:56 error[invalid-argument-type] Argument to function `update_legend` is incorrect: Expected `GlyphRenderer[Glyph]`, found `GlyphRenderer[GlyphType@GlyphRenderer]`
+ src/bokeh/plotting/_renderer.py:141:12 error[invalid-return-type] Return type does not match returned value: expected `GlyphRenderer[Glyph]`, found `GlyphRenderer[GlyphType@GlyphRenderer]`

discord.py (https://github.com/Rapptz/discord.py)
- discord/permissions.py:216:48 warning[unused-type-ignore-comment] Unused blanket `type: ignore` directive
- discord/permissions.py:488:46 warning[unused-type-ignore-comment] Unused blanket `type: ignore` directive
+ discord/shard.py:380:50 error[unknown-argument] Argument `shard_connect_timeout` does not match any known parameter of `Client.__init__`
+ discord/shard.py:380:50 error[unknown-argument] Argument `shard_ids` does not match any known parameter of `Client.__init__`
+ discord/shard.py:380:50 error[invalid-argument-type] Argument to `Client.__init__` is incorrect: Expected `Status | None`, found `int | None`
+ discord/shard.py:380:50 error[invalid-argument-type] Argument to `Client.__init__` is incorrect: Expected `int | None`, found `int | float | None`
- discord/ext/commands/bot.py:294:59 warning[unused-type-ignore-comment] Unused blanket `type: ignore` directive
- discord/ext/commands/bot.py:306:48 warning[unused-type-ignore-comment] Unused blanket `type: ignore` directive
- discord/ext/commands/bot.py:307:107 warning[unused-type-ignore-comment] Unused blanket `type: ignore` directive
- discord/ext/commands/bot.py:318:57 warning[unused-type-ignore-comment] Unused blanket `type: ignore` directive
- discord/ext/commands/bot.py:330:48 warning[unused-type-ignore-comment] Unused blanket `type: ignore` directive
- discord/ext/commands/bot.py:331:105 warning[unused-type-ignore-comment] Unused blanket `type: ignore` directive
- discord/ext/commands/core.py:1551:48 warning[unused-type-ignore-comment] Unused blanket `type: ignore` directive
- discord/ext/commands/core.py:1608:48 warning[unused-type-ignore-comment] Unused blanket `type: ignore` directive
+ discord/ext/commands/core.py:602:38 error[invalid-argument-type] Argument to `Command.__init__` is incorrect: Expected `CooldownMapping[Context[Any]]`, found `Any | list[str] | tuple[str, ...] | ... omitted 7 union elements`
+ discord/ext/commands/core.py:602:38 error[invalid-argument-type] Argument to `Command.__init__` is incorrect: Expected `MaxConcurrency`, found `Any | list[str] | tuple[str, ...] | ... omitted 7 union elements`
+ discord/ext/commands/core.py:602:38 error[invalid-argument-type] Argument to `Command.__init__` is incorrect: Expected `bool`, found `Any | list[str] | tuple[str, ...] | ... omitted 7 union elements`
+ discord/ext/commands/core.py:602:38 error[invalid-argument-type] Argument to `Command.__init__` is incorrect: Expected `bool`, found `Any | list[str] | tuple[str, ...] | ... omitted 7 union elements`
+ discord/ext/commands/core.py:602:38 error[invalid-argument-type] Argument to `Command.__init__` is incorrect: Expected `bool`, found `Any | list[str] | tuple[str, ...] | ... omitted 7 union elements`
+ discord/ext/commands/core.py:602:38 error[invalid-argument-type] Argument to `Command.__init__` is incorrect: Expected `bool`, found `Any | list[str] | tuple[str, ...] | ... omitted 7 union elements`
+ discord/ext/commands/core.py:602:38 error[invalid-argument-type] Argument to `Command.__init__` is incorrect: Expected `bool`, found `Any | list[str] | tuple[str, ...] | ... omitted 7 union elements`
+ discord/ext/commands/core.py:602:38 error[invalid-argument-type] Argument to `Command.__init__` is incorrect: Expected `bool`, found `Any | list[str] | tuple[str, ...] | ... omitted 7 union elements`
+ discord/ext/commands/core.py:602:38 error[invalid-argument-type] Argument to `Command.__init__` is incorrect: Expected `dict[Any, Any]`, found `Any | list[str] | tuple[str, ...] | ... omitted 7 union elements`
+ discord/ext/commands/core.py:602:38 error[invalid-argument-type] Argument to `Command.__init__` is incorrect: Expected `list[(Context[Any], /) -> bool | Coroutine[Any, Any, bool]]`, found `Any | list[str] | tuple[str, ...] | ... omitted 7 union elements`
+ discord/ext/commands/core.py:602:38 error[invalid-argument-type] Argument to `Command.__init__` is incorrect: Expected `list[str] | tuple[str, ...]`, found `Any | list[str] | tuple[str, ...] | ... omitted 7 union elements`
+ discord/ext/commands/core.py:602:38 error[invalid-argument-type] Argument to `Command.__init__` is incorrect: Expected `str | None`, found `Any | list[str] | tuple[str, ...] | ... omitted 7 union elements`
+ discord/ext/commands/core.py:602:38 error[invalid-argument-type] Argument to `Command.__init__` is incorrect: Expected `str | None`, found `Any | list[str] | tuple[str, ...] | ... omitted 7 union elements`
+ discord/ext/commands/core.py:602:38 error[invalid-argument-type] Argument to `Command.__init__` is incorrect: Expected `str | None`, found `Any | list[str] | tuple[str, ...] | ... omitted 7 union elements`
+ discord/ext/commands/core.py:602:38 error[invalid-argument-type] Argument to `Command.__init__` is incorrect: Expected `str`, found `Any | list[str] | tuple[str, ...] | ... omitted 7 union elements`
+ discord/ext/commands/core.py:602:38 error[invalid-argument-type] Argument to `Command.__init__` is incorrect: Expected `str`, found `Any | list[str] | tuple[str, ...] | ... omitted 7 union elements`
+ discord/ext/commands/core.py:1859:12 error[no-matching-overload] No overload of function `command` matches arguments
+ discord/ext/commands/help.py:1087:26 error[unknown-argument] Argument `arguments_heading` does not match any known parameter of `HelpCommand.__init__`
+ discord/ext/commands/help.py:1087:26 error[unknown-argument] Argument `commands_heading` does not match any known parameter of `HelpCommand.__init__`
+ discord/ext/commands/help.py:1087:26 error[unknown-argument] Argument `default_argument_description` does not match any known parameter of `HelpCommand.__init__`
+ discord/ext/commands/help.py:1087:26 error[unknown-argument] Argument `dm_help_threshold` does not match any known parameter of `HelpCommand.__init__`
+ discord/ext/commands/help.py:1087:26 error[unknown-argument] Argument `dm_help` does not match any known parameter of `HelpCommand.__init__`
+ discord/ext/commands/help.py:1087:26 error[unknown-argument] Argument `indent` does not match any known parameter of `HelpCommand.__init__`
+ discord/ext/commands/help.py:1087:26 error[unknown-argument] Argument `no_category` does not match any known parameter of `HelpCommand.__init__`
+ discord/ext/commands/help.py:1087:26 error[unknown-argument] Argument `paginator` does not match any known parameter of `HelpCommand.__init__`
+ discord/ext/commands/help.py:1087:26 error[unknown-argument] Argument `show_parameter_descriptions` does not match any known parameter of `HelpCommand.__init__`
+ discord/ext/commands/help.py:1087:26 error[unknown-argument] Argument `sort_commands` does not match any known parameter of `HelpCommand.__init__`
+ discord/ext/commands/help.py:1087:26 error[unknown-argument] Argument `width` does not match any known parameter of `HelpCommand.__init__`
+ discord/ext/commands/help.py:1087:26 error[invalid-argument-type] Argument to `HelpCommand.__init__` is incorrect: Expected `_CommandKwargs`, found `str`
+ discord/ext/commands/help.py:1087:26 error[invalid-argument-type] Argument to `HelpCommand.__init__` is incorrect: Expected `bool | None`, found `str`
+ discord/ext/commands/help.py:1087:26 error[invalid-argument-type] Argument to `HelpCommand.__init__` is incorrect: Expected `bool`, found `_CommandKwargs`
+ discord/ext/commands/help.py:1378:26 error[unknown-argument] Argument `aliases_heading` does not match any known parameter of `HelpCommand.__init__`
+ discord/ext/commands/help.py:1378:26 error[unknown-argument] Argument `commands_heading` does not match any known parameter of `HelpCommand.__init__`
+ discord/ext/commands/help.py:1378:26 error[unknown-argument] Argument `dm_help_threshold` does not match any known parameter of `HelpCommand.__init__`
+ discord/ext/commands/help.py:1378:26 error[unknown-argument] Argument `dm_help` does not match any known parameter of `HelpCommand.__init__`
+ discord/ext/commands/help.py:1378:26 error[unknown-argument] Argument `no_category` does not match any known parameter of `HelpCommand.__init__`
+ discord/ext/commands/help.py:1378:26 error[unknown-argument] Argument `paginator` does not match any known parameter of `HelpCommand.__init__`
+ discord/ext/commands/help.py:1378:26 error[unknown-argument] Argument `sort_commands` does not match any known parameter of `HelpCommand.__init__`
+ discord/ext/commands/help.py:1378:26 error[invalid-argument-type] Argument to `HelpCommand.__init__` is incorrect: Expected `_CommandKwargs`, found `str`
+ discord/ext/commands/help.py:1378:26 error[invalid-argument-type] Argument to `HelpCommand.__init__` is incorrect: Expected `bool | None`, found `str`
+ discord/ext/commands/help.py:1378:26 error[invalid-argument-type] Argument to `HelpCommand.__init__` is incorrect: Expected `bool`, found `_CommandKwargs`
- discord/ext/commands/hybrid.py:517:50 warning[unused-type-ignore-comment] Unused blanket `type: ignore` directive
- discord/ext/commands/hybrid.py:641:47 warning[unused-type-ignore-comment] Unused blanket `type: ignore` directive
- discord/ext/commands/hybrid.py:849:59 warning[unused-type-ignore-comment] Unused blanket `type: ignore` directive
- discord/ext/commands/hybrid.py:861:48 warning[unused-type-ignore-comment] Unused blanket `type: ignore` directive
- discord/ext/commands/hybrid.py:862:107 warning[unused-type-ignore-comment] Unused blanket `type: ignore` directive
- discord/ext/commands/hybrid.py:873:57 warning[unused-type-ignore-comment] Unused blanket `type: ignore` directive
- discord/ext/commands/hybrid.py:885:48 warning[unused-type-ignore-comment] Unused blanket `type: ignore` directive
- discord/ext/commands/hybrid.py:886:105 warning[unused-type-ignore-comment] Unused blanket `type: ignore` directive
- discord/ext/commands/hybrid.py:897:54 warning[unused-type-ignore-comment] Unused blanket `type: ignore` directive
- discord/ext/commands/hybrid.py:940:92 warning[unused-type-ignore-comment] Unused blanket `type: ignore` directive
- discord/ext/commands/hybrid.py:949:52 warning[unused-type-ignore-comment] Unused blanket `type: ignore` directive
- discord/ext/commands/hybrid.py:973:90 warning[unused-type-ignore-comment] Unused blanket `type: ignore` directive
+ discord/ext/commands/hybrid.py:528:32 error[unknown-argument] Argument `default_permissions` does not match any known parameter of `Command.__init__`
+ discord/ext/commands/hybrid.py:528:32 error[unknown-argument] Argument `guild_ids` does not match any known parameter of `Command.__init__`
+ discord/ext/commands/hybrid.py:528:32 error[unknown-argument] Argument `guild_only` does not match any known parameter of `Command.__init__`
+ discord/ext/commands/hybrid.py:528:32 error[unknown-argument] Argument `nsfw` does not match any known parameter of `Command.__init__`
+ discord/ext/commands/hybrid.py:528:32 error[unknown-argument] Argument `with_app_command` does not match any known parameter of `Command.__init__`
+ discord/ext/commands/hybrid.py:528:32 error[invalid-argument-type] Argument to `Command.__init__` is incorrect: Expected `MaxConcurrency`, found `str | None`
+ discord/ext/commands/hybrid.py:528:32 error[invalid-argument-type] Argument to `Command.__init__` is incorrect: Expected `bool`, found `MaxConcurrency`
+ discord/ext/commands/hybrid.py:528:32 error[invalid-argument-type] Argument to `Command.__init__` is incorrect: Expected `bool`, found `list[int]`
+ discord/ext/commands/hybrid.py:528:32 error[invalid-argument-type] Argument to `Command.__init__` is incorrect: Expected `bool`, found `str`
+ discord/ext/commands/hybrid.py:528:32 error[invalid-argument-type] Argument to `Command.__init__` is incorrect: Expected `dict[Any, Any]`, found `bool`
+ discord/ext/commands/hybrid.py:528:32 error[invalid-argument-type] Argument to `Command.__init__` is incorrect: Expected `str | None`, found `dict[Any, Any]`
+ discord/ext/commands/hybrid.py:528:32 error[invalid-argument-type] Argument to `Command.__init__` is incorrect: Expected `str`, found `bool`
+ discord/ext/commands/hybrid.py:528:32 error[invalid-argument-type] Argument to `Command.__init__` is incorrect: Expected `str`, found `bool`
+ discord/ext/commands/hybrid.py:651:33 error[unknown-argument] Argument `default_permissions` does not match any known parameter of `Group.__init__`
+ discord/ext/commands/hybrid.py:651:33 error[unknown-argument] Argument `guild_ids` does not match any known parameter of `Group.__init__`
+ discord/ext/commands/hybrid.py:651:33 error[unknown-argument] Argument `guild_only` does not match any known parameter of `Group.__init__`
+ discord/ext/commands/hybrid.py:651:33 error[unknown-argument] Argument `nsfw` does not match any known parameter of `Group.__init__`
+ discord/ext/commands/hybrid.py:651:33 error[unknown-argument] Argument `with_app_command` does not match any known parameter of `Group.__init__`
+ discord/ext/commands/hybrid.py:651:33 error[invalid-argument-type] Argument to `Group.__init__` is incorrect: Expected `MaxConcurrency`, found `str | None`
+ discord/ext/commands/hybrid.py:651:33 error[invalid-argument-type] Argument to `Group.__init__` is incorrect: Expected `bool`, found `MaxConcurrency`
+ discord/ext/commands/hybrid.py:651:33 error[invalid-argument-type] Argument to `Group.__init__` is incorrect: Expected `bool`, found `list[int]`
+ discord/ext/commands/hybrid.py:651:33 error[invalid-argument-type] Argument to `Group.__init__` is incorrect: Expected `bool`, found `str`
+ discord/ext/commands/hybrid.py:651:33 error[invalid-argument-type] Argument to `Group.__init__` is incorrect: Expected `dict[Any, Any]`, found `bool`
+ discord/ext/commands/hybrid.py:651:33 error[invalid-argument-type] Argument to `Group.__init__` is incorrect: Expected `str | None`, found `dict[Any, Any]`
+ discord/ext/commands/hybrid.py:651:33 error[invalid-argument-type] Argument to `Group.__init__` is incorrect: Expected `str`, found `bool`
+ discord/ext/commands/hybrid.py:651:33 error[invalid-argument-type] Argument to `Group.__init__` is incorrect: Expected `str`, found `bool`
+ discord/ext/commands/hybrid.py:844:9 error[invalid-method-override] Invalid override of method `command`: Definition is incompatible with `GroupMixin.command`
+ discord/ext/commands/hybrid.py:868:9 error[invalid-method-override] Invalid override of method `group`: Definition is incompatible with `GroupMixin.group`

hydpy (https://github.com/hydpy-dev/hydpy)
+ hydpy/models/rconc/rconc_control.py:249:23 error[no-matching-overload] No overload of `dict.__init__` matches arguments

pandas (https://github.com/pandas-dev/pandas)
- pandas/io/parsers/readers.py:1604:41 error[invalid-argument-type] Argument to function `len` is incorrect: Expected `Sized`, found `(@Todo & ~Literal[False] & ~_NoDefault) | None`
+ pandas/io/parsers/readers.py:1604:41 error[invalid-argument-type] Argument to function `len` is incorrect: Expected `Sized`, found `Hashable & ~Literal[False] & ~_NoDefault`
+ pandas/tests/io/parser/test_read_fwf.py:986:26 error[invalid-argument-type] Argument to function `read_fwf` is incorrect: Expected `Literal["pyarrow", "numpy_nullable"] | _NoDefault`, found `Literal["numpy"]`
+ pandas/tests/io/parser/test_unsupported.py:43:17 error[no-matching-overload] No overload of function `read_csv` matches arguments
+ pandas/tests/io/test_common.py:442:13 error[no-matching-overload] No overload of function `read_csv` matches arguments

prefect (https://github.com/PrefectHQ/prefect)
+ src/integrations/prefect-dbt/prefect_dbt/core/_orchestrator.py:1916:25 error[invalid-argument-type] Argument to `MaterializingTask.__init__` is incorrect: Expected `list[Asset | str] | None`, found `(list[Asset] & ~AlwaysFalsy) | None`
+ src/integrations/prefect-gitlab/prefect_gitlab/repositories.py:90:32 error[no-matching-overload] No overload of function `Field` matches arguments
+ src/prefect/tasks.py:2218:9 error[invalid-method-override] Invalid override of method `with_options`: Definition is incompatible with `Task.with_options`

pydantic (https://github.com/pydantic/pydantic)
- pydantic/fields.py:239:95 warning[unused-type-ignore-comment] Unused blanket `type: ignore` directive
- pydantic/fields.py:278:57 warning[unused-type-ignore-comment] Unused blanket `type: ignore` directive
- pydantic/fields.py:1278:39 warning[unused-type-ignore-comment] Unused blanket `type: ignore` directive
- pydantic/fields.py:1282:47 warning[unused-type-ignore-comment] Unused blanket `type: ignore` directive
- pydantic/fields.py:1292:47 warning[unused-type-ignore-comment] Unused blanket `type: ignore` directive
- pydantic/fields.py:1302:53 warning[unused-type-ignore-comment] Unused blanket `type: ignore` directive
- pydantic/fields.py:1312:57 warning[unused-type-ignore-comment] Unused blanket `type: ignore` directive
- pydantic/fields.py:1322:39 warning[unused-type-ignore-comment] Unused blanket `type: ignore` directive
- pydantic/fields.py:1335:40 warning[unused-type-ignore-comment] Unused blanket `type: ignore` directive
- pydantic/fields.py:1350:43 warning[unused-type-ignore-comment] Unused blanket `type: ignore` directive
+ pydantic/fields.py:669:16 error[no-matching-overload] No overload of function `Field` matches arguments

pyinstrument (https://github.com/joerick/pyinstrument)
- pyinstrument/context_manager.py:41:9 error[invalid-assignment] Object of type `dict[str, @Todo]` is not assignable to attribute `options` of type `ProfileContextOptions`

pywin32 (https://github.com/mhammond/pywin32)
+ com/win32comext/shell/demos/servers/folder_view.py:855:9 error[invalid-argument-type] Argument to function `UseCommandLine` is incorrect: Expected `bool`, found `Literal[0]`
+ com/win32comext/shell/demos/servers/shell_view.py:968:9 error[invalid-argument-type] Argument to function `UseCommandLine` is incorrect: Expected `bool`, found `Literal[0]`

rotki (https://github.com/rotki/rotki)
+ rotkehlchen/tests/db/test_db.py:407:20 error[no-matching-overload] No overload of bound method `DBHandler.get_dynamic_cache` matches arguments
+ rotkehlchen/tests/db/test_db.py:447:20 error[no-matching-overload] No overload of bound method `DBHandler.get_dynamic_cache` matches arguments

scipy-stubs (https://github.com/scipy/scipy-stubs)
- tests/stats/test_new_distributions.pyi:15:1 error[type-assertion-failure] Type `Normal[tuple[()], float64]` does not match asserted type `Normal[tuple[int], floating]`

trio (https://github.com/python-trio/trio)
- src/trio/_tests/test_subprocess.py:497:55 warning[unused-type-ignore-comment] Unused blanket `type: ignore` directive
- src/trio/_tests/type_tests/subprocesses.py:15:70 warning[unused-type-ignore-comment] Unused blanket `type: ignore` directive
- src/trio/_tests/type_tests/subprocesses.py:22:60 warning[unused-type-ignore-comment] Unused blanket `type: ignore` directive
- src/trio/_tests/type_tests/subprocesses.py:23:70 warning[unused-type-ignore-comment] Unused blanket `type: ignore` directive

Full report with detailed diff (timing results)

@charliermarsh charliermarsh force-pushed the charlie/unpack branch 3 times, most recently from 1b9a9bd to ffd2dca Compare April 15, 2026 14:21
@codspeed-hq
Copy link
Copy Markdown

codspeed-hq Bot commented Apr 15, 2026

Merging this PR will not alter performance

✅ 53 untouched benchmarks
⏩ 60 skipped benchmarks1


Comparing charlie/unpack (a407396) with main (9b03f2b)

Open in CodSpeed

Footnotes

  1. 60 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

has_starred_annotation: bool,

/// Whether this parameter was declared as `**kwargs: Unpack[TypedDict]`.
has_unpacked_kwargs_annotation: bool,
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This lets us distinguish **kwargs: Unpack[TD] from **kwargs: TD. The latter means that every kwarg value is itself of type TD.

Comment on lines +1018 to +1019
/// Per [PEP 692](https://peps.python.org/pep-0692/#typeddict-unions), unions (for example) are not
/// allowed in such annotations.
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Slightly annoying because we already have some logic for extracting these keys in the more general case (see, e.g., extract_unpacked_typed_dict_keys_from_value_type), but PEP 692 explicitly forbids unions.

/// The argument definitely binds this parameter.
Definitive,
/// The argument may bind this parameter at runtime, but does not guarantee its presence.
Provisional,
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is necessary for cases like...

class MaybeX(TypedDict, total=False):
    x: str

def takes_x(*, x: int) -> None: ...

takes_x(**maybe_x)

Where we need to report both missing-argument and invalid-argument-type.

@charliermarsh
Copy link
Copy Markdown
Member Author

For the ecosystem report...

  • I believe the graphql-core diagnostics are true positives? They're unpacking with a TypedDict(total=False), although according to Codex other type checkers don't raise these errors, i.e., they don't require that the key is definitely present. I prefer our behavior but it is stricter.
  • For bokeh, there are generally a lot of TypedDict usages in that codebase that type checkers can't model (e.g., calls on an event type that then need to narrow the return type, etc.), so I'm not overly concerned.

Based on Codex's analysis, the rest are either true positives or things that are sufficiently dynamic that we shouldn't really expect to model them. But I'll go through a few more on my own.

@charliermarsh
Copy link
Copy Markdown
Member Author

Also, note that the newly-added false positive on the conformance tests is related to extra_items, which we don't yet support.


## `Unpack[TypedDict]` in `**kwargs`

Using `Unpack[TypedDict]` on a `**kwargs` parameter should expose the `TypedDict` shape both inside
Copy link
Copy Markdown
Member

@MichaReiser MichaReiser Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't read the mdtest but could we split this test into multiple smaller snippets with some prose between snippets explaining what's being tested and what the expected behavior is?

This is one of my main learnings from maintaining Ruff. The long fixture files are a pain to maintain over time because they lack context of why something has been tested in the first place and if it's even asserting something intentionally or if they just tried to be exhaustive.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes sir!

Copy link
Copy Markdown
Contributor

@carljm carljm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is awesome! Will be great to have this feature.

I reviewed the tests and some of the implementation; there are enough findings that might change enough of the implementation that I think it makes sense to do an iteration before reviewing more.

One meta-comment is that I think this is a case where the PR might have (understandably) suffered a bit of scope-creep. I think there are really two separable things addressed here. One is TypedDict unpacking on the formal-parameter side using Unpack (which is really just a matter of signature transformation, and a bit of code to infer the right type within the function body). The other is the whole Provisional matched argument thing, which is all about caller-side actual-argument unpacked TypedDict, and doesn't actually care whether the formal parameter is from an Unpack[TD] or just regular keyword arguments.

I think it would clarify the PR and the review if we separated those concerns into separate PRs.

Comment thread crates/ty_python_semantic/resources/mdtest/typed_dict.md
Comment on lines +3048 to +3049
explicit_ok: ExplicitKwargs = func
typed_dict_ok: TypedDictKwargs = func
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is good; I'd also love to see this:

def _(explicit: ExplicitKwargs, typed_dict: TypedDictKwargs) -> None:
    typed_dict_2: TypedDictKwargs = explicit
    explicit_2: ExplicitKwargs = typed_dict

I think these two lines should both pass, but currently the first one fails. I believe the reason is the same bug causing this to fail:

from typing import Protocol
from typing_extensions import TypedDict, Unpack

class TD(TypedDict, total=False):
    a: int

def f(**kwargs: Unpack[TD]) -> None:
    pass

class WantsB(Protocol):
    def __call__(self, *, b: TD) -> None: ...

# BUG: this PR accepts this assignment, because it treats the retained
# `**kwargs: TD` as accepting arbitrary keyword names.
p: WantsB = f

# But the actual call surface of `f` does not accept `b`.
# ty correctly reports this as an unknown argument.
f(b={})

I think the root cause of both is that we retain the **kwargs argument in the signature of a callable with **kwargs: Unpack[TD], when really we should treat that callable as if it did not have **kwargs.

Comment on lines +3054 to +3058
def func7(*, v1: int, v3: str, v2: str = "") -> None:
pass

# error: [invalid-assignment]
typed_dict_bad: TypedDictKwargs = func7
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we expect invalid-assignment here? It looks to me like this assignment should succeed. func7 has the same signature that we should be treating TypedDictKwargs as having.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed; this should be accepted.

class TD2(TD1):
v3: Required[str]

def func5(v1: int, **kwargs: Unpack[TD2]) -> None: # error: [invalid-type-form]
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

invalid-type-form feels weird for this one, since the type form Unpack[TD2] is just fine, what's invalid is the overall function signature. But I don't know of any better fitting code, and I don't think this is worth its own dedicated code. The non-Unpack version of this is just a syntax error, and this isn't, so that parallel doesn't help either. So I guess this really is our best option. Other type checkers also tend to use the same code for this as for the cases where the Unpack expression itself is malformed, so at least we're in good company.

Comment on lines +3111 to +3115
### Regression coverage

These cases check a few tricky edges: unpacking non-string-keyed mappings, combining explicit
keyword arguments with unpacked `TypedDict`s, missing required keys from partial `TypedDict`s, and
legacy dunder-style keyword names.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not to be a pain, but this is kind of a weird catch-all. It would be nicer to split these out into separate tests, each with their own prose...

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also wording nit: "legacy dunder-style positional-only parameters"

Comment on lines +821 to +824
/// The key is guaranteed to be present at runtime when the mapping is unpacked.
Guaranteed,
/// The key may be present at runtime when the mapping is unpacked.
Potential,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm... is there a good reason for us (here and in method names below) to invent new terminology that means the same thing as Required and NotRequired, which is the syntax that is actually used in defining a TypedDict key?

from typing import Any
from typing_extensions import TypedDict, Unpack

class AnyKwargs(TypedDict, total=False):
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: I find this name confusing, because there is nothing Any related about this typed-dict type.

if let Some((parameter_index, parameter, _)) =
self.parameters.unpacked_typed_dict_keyword_variadic(db)
{
let permissive_any_mapping = argument_type.is_some_and(|argument_type| {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Commented above on the tests -- I don't think it is correct to specially handle dict[str, Any]. All dict[str, ...] types should be forgiving in argument matching (but of course should still require that ... is assignable to each matched formal parameter, which is always true for Any).

We (and all type checkers) have the same forgiving behavior for e.g. * unpacking of list[int] -- we optimistically assume the list is the right length for the available parameters. But we still require that int be assignable to each matched parameter.

Comment on lines +999 to +1000
/// Per [PEP 692](https://peps.python.org/pep-0692/#typeddict-unions), this accepts only a concrete
/// `TypedDict` target, or a type alias resolving to one.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it is always implied that stringified annotations should be supported too, but this function doesn't support them. All other type checkers support this:

from typing_extensions import TypedDict, Unpack

class TD(TypedDict):
    a: int

def f(**kwargs: "Unpack[TD]") -> None:
    reveal_type(kwargs)  # should be TD

# should be accepted, same as an unquoted `Unpack[TD]`.
f(a=1)

}
}

value.push(keywords);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think in order to fix some of the assignability bugs I mentioned in the tests, and remove the need for some special casing in call binding, we should eliminate this line -- the original **kwargs: Unpack[TD] parameter should not stay as part of the signature proper.

This might (not totally sure without trying it) require keeping a bit (or index?) around somewhere in the signature recording that this transformation happened, so that we can still refer to the original kwargs argument in diagnostics, detect arguments duplicated via this unpacking, etc. But I think it's important that we do that in a dedicated way that doesn't affect the signature itself, and is only used for these additional validation/diagnostic purposes.

(Keeping the **kwargs argument around in the signature is not necessary for properly inferring the type of kwargs inside the function. That goes through a totally separate Definition path that leads directly to the annotation on **kwargs, it doesn't go through a callable signature at all.)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(The normalization is inside Parameters::new, but we now do roughly this.)

@charliermarsh charliermarsh marked this pull request as draft April 18, 2026 03:03
@charliermarsh
Copy link
Copy Markdown
Member Author

Great review, thank you.

@charliermarsh charliermarsh changed the title [ty] Support Unpack[TypedDict] [ty] Support Unpack[TypedDict] in **kwargs signatures Apr 19, 2026
@charliermarsh charliermarsh force-pushed the charlie/unpack branch 3 times, most recently from 7033e83 to a7096ef Compare April 19, 2026 03:03
@charliermarsh
Copy link
Copy Markdown
Member Author

I pulled the validation changes out into #24710 -- sorry about that.

@charliermarsh charliermarsh marked this pull request as ready for review April 19, 2026 03:08
@astral-sh-bot astral-sh-bot Bot requested a review from carljm April 19, 2026 03:08
@charliermarsh
Copy link
Copy Markdown
Member Author

We no longer emit this error from the conformance suite, looking into it:

from typing import Protocol, TypeVar, TypedDict, NotRequired, Required, Unpack, assert_type


class TD1(TypedDict):
    v1: Required[int]
    v2: NotRequired[str]


class TD2(TD1):
    v3: Required[str]


...


class TDProtocol6(Protocol):
    def __call__(self, **kwargs: Unpack[TD2]) -> None:
        ...


...


# > The situation where the destination callable contains **kwargs: Unpack[TypedDict] and
# > the source callable doesn’t contain **kwargs should be disallowed. This is because,
# > we cannot be sure that additional keyword arguments are not being passed in when an instance of a subclass
# > had been assigned to a variable with a base class type and then unpacked in the destination callable invocation

def func7(*, v1: int, v3: str, v2: str = "") -> None:
    ...


v7: TDProtocol6 = func7  # E: source does not have kwargs

@charliermarsh
Copy link
Copy Markdown
Member Author

(Fixed.)

@charliermarsh charliermarsh force-pushed the charlie/unpack branch 2 times, most recently from 0b571b6 to f3253e8 Compare April 22, 2026 15:34
@carljm
Copy link
Copy Markdown
Contributor

carljm commented Apr 22, 2026

Looks like the latest rebase here has some logical conflicts causing it to not compile.

@charliermarsh
Copy link
Copy Markdown
Member Author

Fixed, sorry about that.

Comment on lines +3060 to +3061
# error: [unknown-argument]
func(v1=1, v3="ok", v4=1)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think erroring here is wrong, but the conformance suite currently requires it :/

Wrote up python/typing#2272

Not sure if you'd rather just hold off on this PR until we see what the reaction to that is from other typing council members (I'd expect to get some responses in the next day or two), or if you'd rather land this now (either my preferred way, or the conformance suite way), and then possibly update the behavior later depending what happens with my conformance suite PR).

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that this likely shouldn't error. I'd probably err on the side of just merging and updating later if the PR is otherwise ready, but it's also okay if you'd prefer to wait.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ty Multi-file analysis & type inference

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants