Skip to content

Fix TypedDict get literal defaults#3831

Open
goutamadwant wants to merge 1 commit into
facebook:mainfrom
goutamadwant:fix/pyrefly-3787-typed-dict-get-literal
Open

Fix TypedDict get literal defaults#3831
goutamadwant wants to merge 1 commit into
facebook:mainfrom
goutamadwant:fix/pyrefly-3787-typed-dict-get-literal

Conversation

@goutamadwant

Copy link
Copy Markdown

Summary

Fixes #3787

Add a field-typed default overload when synthesizing TypedDict.get and TypedDict.pop methods:

(key: Literal["field"], default: FieldType) -> FieldType

This overload is tried before the existing generic default overload, so defaults that are already assignable to the field type keep the field type. For a non-required field typed as Literal["a", "b"], .get("x", "b") now resolves to Literal["a", "b"] instead of Literal["a", "b"] | str.

Defaults outside the field type still use the generic _T overload and keep the existing FieldType | T behavior.

Test Plan

cargo fmt
cargo test -p pyrefly test_get_not_required_literal_default -- --nocapture
cargo test -p pyrefly test_get -- --nocapture
cargo test -p pyrefly test_pop -- --nocapture

@meta-cla

meta-cla Bot commented Jun 16, 2026

Copy link
Copy Markdown

Hi @goutamadwant!

Thank you for your pull request and welcome to our community.

Action Required

In order to merge any pull request (code, docs, etc.), we require contributors to sign our Contributor License Agreement, and we don't seem to have one on file for you.

Process

In order for us to review and merge your suggested changes, please sign at https://code.facebook.com/cla. If you are contributing on behalf of someone else (eg your employer), the individual CLA may not be sufficient and your employer may need to sign the corporate CLA.

Once the CLA is signed, our tooling will perform checks and validations. Afterwards, the pull request will be tagged with CLA signed. The tagging process may take up to 1 hour after signing. Please give it that time before contacting us about it.

If you have received this in error or have any questions, please contact us at cla@meta.com. Thanks!

@meta-cla

meta-cla Bot commented Jun 16, 2026

Copy link
Copy Markdown

Thank you for signing our Contributor License Agreement. We can now accept your code for this (and any) Meta Open Source project. Thanks!

@meta-cla meta-cla Bot added the cla signed label Jun 16, 2026
@meta-cla

meta-cla Bot commented Jun 16, 2026

Copy link
Copy Markdown

Thank you for signing our Contributor License Agreement. We can now accept your code for this (and any) Meta Open Source project. Thanks!

@github-actions

Copy link
Copy Markdown

Diff from mypy_primer, showing the effect of this PR on open source code:

pyinstrument (https://github.com/joerick/pyinstrument)
- ERROR pyinstrument/context_manager.py:40:34-52: Argument `Literal['disabled', 'enabled', 'strict'] | str` is not assignable to parameter `async_mode` with type `Literal['disabled', 'enabled', 'strict']` in function `pyinstrument.profiler.Profiler.__init__` [bad-argument-type]

meson (https://github.com/mesonbuild/meson)
- ERROR mesonbuild/build.py:2198:32-68: `Literal['bin', 'cdylib', 'dylib', 'lib', 'proc-macro', 'rlib', 'staticlib'] | str` is not assignable to attribute `rust_crate_type` with type `Literal['bin', 'cdylib', 'dylib', 'lib', 'proc-macro', 'rlib', 'staticlib']` [bad-assignment]
- ERROR mesonbuild/build.py:2331:32-69: `Literal['bin', 'cdylib', 'dylib', 'lib', 'proc-macro', 'rlib', 'staticlib'] | str` is not assignable to attribute `rust_crate_type` with type `Literal['bin', 'cdylib', 'dylib', 'lib', 'proc-macro', 'rlib', 'staticlib']` [bad-assignment]
- ERROR mesonbuild/build.py:2551:32-70: `Literal['bin', 'cdylib', 'dylib', 'lib', 'proc-macro', 'rlib', 'staticlib'] | str` is not assignable to attribute `rust_crate_type` with type `Literal['bin', 'cdylib', 'dylib', 'lib', 'proc-macro', 'rlib', 'staticlib']` [bad-assignment]

core (https://github.com/home-assistant/core)
- ERROR homeassistant/components/mqtt/config_flow.py:4648:27-65: No matching overload found for function `homeassistant.components.mqtt.models.MqttDeviceData.update` called with arguments: (dict[tuple[Literal['configuration_url'], str] | tuple[Literal['hw_version'], str] | tuple[Literal['identifiers'], str] | tuple[Literal['model'], str] | tuple[Literal['model_id'], str] | tuple[Literal['mqtt_settings'], DeviceMqttOptions] | tuple[Literal['name'], str] | tuple[Literal['sw_version'], str], @_] | DeviceMqttOptions) [no-matching-overload]
+ ERROR homeassistant/components/mqtt/config_flow.py:4648:27-65: No matching overload found for function `homeassistant.components.mqtt.models.MqttDeviceData.update` called with arguments: (DeviceMqttOptions) [no-matching-overload]
- ERROR homeassistant/loader.py:860:16-60: Returned type `Literal['device', 'entity', 'hardware', 'helper', 'hub', 'service', 'system', 'virtual'] | str` is not assignable to declared return type `Literal['device', 'entity', 'hardware', 'helper', 'hub', 'service', 'system', 'virtual']` [bad-return]

paasta (https://github.com/yelp/paasta)
- ERROR paasta_tools/kubernetes_tools.py:820:35-85: No matching overload found for function `typing.MutableMapping.update` called with arguments: (dict[str, int | list[dict[str, int | str]] | str] | dict[Unknown, Unknown] | None) [no-matching-overload]
+ ERROR paasta_tools/kubernetes_tools.py:820:35-85: No matching overload found for function `typing.MutableMapping.update` called with arguments: (dict[Unknown, Unknown] | None) [no-matching-overload]

discord.py (https://github.com/Rapptz/discord.py)
- ERROR discord/app_commands/tree.py:1221:63-87: Argument `dict[@_, @_] | ResolvedData` is not assignable to parameter `resolved` with type `ResolvedData` in function `discord.app_commands.namespace.Namespace._get_resolved_items` [bad-argument-type]
- ERROR discord/app_commands/tree.py:1277:44-68: Argument `dict[@_, @_] | ResolvedData` is not assignable to parameter `resolved` with type `ResolvedData` in function `discord.app_commands.namespace.Namespace.__init__` [bad-argument-type]
- ERROR discord/ext/commands/help.py:402:53-73: Unpacked keyword argument `object` is not assignable to parameter `enabled` with type `bool` in function `_HelpCommandImpl.__init__` [bad-argument-type]
- ERROR discord/ext/commands/help.py:402:53-73: Unpacked keyword argument `object` is not assignable to parameter `help` with type `str | None` in function `_HelpCommandImpl.__init__` [bad-argument-type]
- ERROR discord/ext/commands/help.py:402:53-73: Unpacked keyword argument `object` is not assignable to parameter `brief` with type `str | None` in function `_HelpCommandImpl.__init__` [bad-argument-type]
- ERROR discord/ext/commands/help.py:402:53-73: Unpacked keyword argument `object` is not assignable to parameter `usage` with type `str | None` in function `_HelpCommandImpl.__init__` [bad-argument-type]
- ERROR discord/ext/commands/help.py:402:53-73: Unpacked keyword argument `object` is not assignable to parameter `rest_is_raw` with type `bool` in function `_HelpCommandImpl.__init__` [bad-argument-type]
- ERROR discord/ext/commands/help.py:402:53-73: Unpacked keyword argument `object` is not assignable to parameter `aliases` with type `list[str] | tuple[str, ...]` in function `_HelpCommandImpl.__init__` [bad-argument-type]
- ERROR discord/ext/commands/help.py:402:53-73: Unpacked keyword argument `object` is not assignable to parameter `description` with type `str` in function `_HelpCommandImpl.__init__` [bad-argument-type]
- ERROR discord/ext/commands/help.py:402:53-73: Unpacked keyword argument `object` is not assignable to parameter `hidden` with type `bool` in function `_HelpCommandImpl.__init__` [bad-argument-type]
- ERROR discord/ext/commands/help.py:402:53-73: Unpacked keyword argument `object` is not assignable to parameter `checks` with type `list[(Context[Any]) -> Coroutine[Any, Any, bool] | bool]` in function `_HelpCommandImpl.__init__` [bad-argument-type]
- ERROR discord/ext/commands/help.py:402:53-73: Unpacked keyword argument `object` is not assignable to parameter `cooldown` with type `CooldownMapping[Context[Any]]` in function `_HelpCommandImpl.__init__` [bad-argument-type]
- ERROR discord/ext/commands/help.py:402:53-73: Unpacked keyword argument `object` is not assignable to parameter `max_concurrency` with type `MaxConcurrency` in function `_HelpCommandImpl.__init__` [bad-argument-type]
- ERROR discord/ext/commands/help.py:402:53-73: Unpacked keyword argument `object` is not assignable to parameter `require_var_positional` with type `bool` in function `_HelpCommandImpl.__init__` [bad-argument-type]
- ERROR discord/ext/commands/help.py:402:53-73: Unpacked keyword argument `object` is not assignable to parameter `cooldown_after_parsing` with type `bool` in function `_HelpCommandImpl.__init__` [bad-argument-type]
- ERROR discord/ext/commands/help.py:402:53-73: Unpacked keyword argument `object` is not assignable to parameter `ignore_extra` with type `bool` in function `_HelpCommandImpl.__init__` [bad-argument-type]
- ERROR discord/ext/commands/help.py:402:53-73: Unpacked keyword argument `object` is not assignable to parameter `extras` with type `dict[Any, Any]` in function `_HelpCommandImpl.__init__` [bad-argument-type]
- ERROR discord/ext/commands/help.py:402:53-73: Unpacked keyword argument `object` is not assignable to parameter `name` with type `str` in function `_HelpCommandImpl.__init__` [bad-argument-type]
- ERROR discord/interactions.py:353:32-56: Argument `dict[@_, @_] | ResolvedData` is not assignable to parameter `resolved` with type `ResolvedData` in function `discord.app_commands.namespace.Namespace.__init__` [bad-argument-type]
- ERROR discord/state.py:830:81-89: Argument `dict[Unknown, Unknown] | ResolvedData` is not assignable to parameter `resolved` with type `ResolvedData` in function `discord.ui.view.ViewStore.dispatch_modal` [bad-argument-type]

@github-actions

Copy link
Copy Markdown

Primer Diff Classification

✅ 3 improvement(s) | ➖ 1 neutral | ❓ 1 needs review | 5 project(s) total | +2, -12 errors

3 improvement(s) across pyinstrument, meson, discord.py.

Project Verdict Changes Error Kinds Root Cause
pyinstrument ✅ Improvement -1 bad-argument-type get_overload_with_value_default()
meson ✅ Improvement -3 bad-assignment get_overload_with_value_default()
core ❓ Needs Review +1, -2 bad-return, no-matching-overload
paasta ➖ Neutral +1, -1 no-matching-overload
discord.py ✅ Improvement -5 bad-argument-type get_overload_with_value_default()
Detailed analysis

✅ Improvement (3)

pyinstrument (-1)

This is a false positive removal. The ProfileContextOptions TypedDict has async_mode: AsyncMode where AsyncMode = Literal['disabled', 'enabled', 'strict']. On line 37, kwargs.get('async_mode', 'disabled') is called. The default value 'disabled' is a valid member of AsyncMode (i.e., Literal['disabled'] is assignable to Literal['disabled', 'enabled', 'strict']). The old type checker behavior incorrectly widened the return type to Literal['disabled', 'enabled', 'strict'] | str, treating the default's type as the general str type rather than recognizing it as already assignable to the field type. This widened type was then placed into the profiler_options dict, and when Profiler(**profiler_options) was called on line 40, the async_mode parameter expected Literal['disabled', 'enabled', 'strict'] but received Literal['disabled', 'enabled', 'strict'] | str, causing the bad-argument-type error. The PR fix correctly preserves the field type as the return type of .get() when the default is assignable to it, eliminating this false positive.
Attribution: The change in pyrefly/lib/alt/class/typed_dict.rs adds a new get_overload_with_value_default() method and inserts it as an overload before the existing generic _T overload in both the get and pop method synthesis paths (around lines 576 and 665 in the new code). This new overload has signature (self, key: Literal["field"], default: FieldType) -> FieldType, which matches when the default value is assignable to the field type, preventing the type from being widened to FieldType | T.

meson (-3)

These were false positives caused by TypedDict.get() unnecessarily widening the return type. When calling kwargs.get('rust_crate_type', 'bin') where rust_crate_type has type Literal['bin', 'lib', 'rlib', 'dylib', 'cdylib', 'staticlib', 'proc-macro'] and the default 'bin' is a member of that Literal, the return type should be Literal['bin', 'lib', 'rlib', 'dylib', 'cdylib', 'staticlib', 'proc-macro'], not Literal['bin', 'lib', 'rlib', 'dylib', 'cdylib', 'staticlib', 'proc-macro'] | str. The PR correctly adds a field-typed default overload to prevent this widening.
Attribution: The change to get_overload_with_value_default() in pyrefly/lib/alt/class/typed_dict.rs adds a new overload (key: Literal["field"], default: FieldType) -> FieldType that is tried before the generic _T overload. This prevents type widening when the default is already assignable to the field type, removing the false positive bad-assignment errors.

discord.py (-5)

Four of the 5 removed errors (errors 1, 2, 4, 5) were false positives caused by TypedDict .get() returning overly broad union types. The ApplicationCommandInteractionData is a TypedDict, and calling .get('resolved', {}) on it produces a type like dict[@_, @_] | ResolvedData because the type checker unions the field type (ResolvedData) with the type of the default value (dict). The PR improves TypedDict get/pop overload resolution so that when the default value's type is already assignable to the field type, the return type is narrowed to just the field type, eliminating the spurious bad-argument-type errors.

Error 3 (discord/ext/commands/help.py:402) is a different kind of error involving unpacked keyword arguments (Unpacked keyword argument 'object' is not assignable to parameter 'enabled' with type 'bool'), which is not a TypedDict .get() issue. This error likely involves a TypedDict being unpacked as **kwargs where the type checker was computing an overly broad type for the unpacked values. The PR's improvements to TypedDict method handling may also resolve this by better narrowing the types of TypedDict values.

All 5 errors appear to be false positives that are resolved by improved TypedDict type inference in the PR.

Attribution: The change in pyrefly/lib/alt/class/typed_dict.rs in the get_overload_with_value_default() method and its insertion into the overload resolution order (before the generic _T overload) directly caused these removals. When data.get('resolved', {}) is called on a TypedDict, the new (key, default: FieldType) -> FieldType overload matches first when the default {} is assignable to the field type, preventing the type from being widened to FieldType | T.

➖ Neutral (1)

paasta (+1, -1)

Same errors at same locations with same error kinds — message wording changed, no behavioral impact.

❓ Needs Review (1)

core (+1, -2)

LLM requested additional files that could not be resolved. Non-trivial change (1 added, 2 removed).


Was this helpful? React with 👍 or 👎

Classification by primer-classifier (1 heuristic, 4 LLM)

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

.get(key, "literal") on a non-required key with TypedDict resolves to key_type | str

1 participant