Skip to content

[ty] Add support for dict literals and dict() calls as default values for parameters with TypedDict types#22161

Merged
ibraheemdev merged 1 commit intoastral-sh:mainfrom
Hugo-Polloli:typed-dict-as-call-parameter
Jan 23, 2026
Merged

[ty] Add support for dict literals and dict() calls as default values for parameters with TypedDict types#22161
ibraheemdev merged 1 commit intoastral-sh:mainfrom
Hugo-Polloli:typed-dict-as-call-parameter

Conversation

@Hugo-Polloli
Copy link
Contributor

Summary

Fixes astral-sh/ty#2161

  • Infer parameter default expressions using the parameter annotation as type context.
  • Fix invalid-parameter-default false positives for TypedDict defaults (literals {...} and calls dict(...)).
  • Add mdtests covering valid and invalid TypedDict defaults plus a non-TypedDict assignability case.

Test Plan

  • Assert no diagnostics for Foo defaults via {"x": 42} and dict(x=42)

  • Assert invalid-parameter-default for missing key, wrong value type, extra key in Foo defaults and non-assignable default (tuple[int] = ())

@astral-sh-bot
Copy link

astral-sh-bot bot commented Dec 23, 2025

Typing conformance results

No changes detected ✅

@astral-sh-bot
Copy link

astral-sh-bot bot commented Dec 23, 2025

mypy_primer results

Changes were detected when running on open source projects
mypy (https://github.com/python/mypy)
- mypy/typeshed/stdlib/tkinter/__init__.pyi:584:9: error[invalid-parameter-default] Default value of type `dict[Unknown, Unknown]` is not assignable to annotated parameter type `_GridIndexInfo`
- mypy/typeshed/stdlib/tkinter/__init__.pyi:594:9: error[invalid-parameter-default] Default value of type `dict[Unknown, Unknown]` is not assignable to annotated parameter type `_GridIndexInfo`
- Found 1744 diagnostics
+ Found 1742 diagnostics

trio (https://github.com/python-trio/trio)
- src/trio/_core/_ki.py:130:13: error[invalid-parameter-default] Default value of type `ReferenceType[Self@__init__]` is not assignable to annotated parameter type `ReferenceType[WeakKeyIdentityDictionary[_KT@WeakKeyIdentityDictionary, _VT@WeakKeyIdentityDictionary]]`
- Found 487 diagnostics
+ Found 486 diagnostics

prefect (https://github.com/PrefectHQ/prefect)
- src/integrations/prefect-dbt/prefect_dbt/core/settings.py:94:28: error[invalid-assignment] Object of type `T@resolve_block_document_references | dict[str, Any]` is not assignable to `dict[str, Any]`
+ src/integrations/prefect-dbt/prefect_dbt/core/settings.py:94:28: error[invalid-assignment] Object of type `dict[str, Any] | int | T@resolve_block_document_references | ... omitted 4 union elements` is not assignable to `dict[str, Any]`
- src/integrations/prefect-dbt/prefect_dbt/core/settings.py:99:28: error[invalid-assignment] Object of type `T@resolve_variables | dict[str, Any]` is not assignable to `dict[str, Any]`
+ src/integrations/prefect-dbt/prefect_dbt/core/settings.py:99:28: error[invalid-assignment] Object of type `int | T@resolve_variables | float | ... omitted 4 union elements` is not assignable to `dict[str, Any]`
- src/prefect/cli/deploy/_core.py:86:21: error[invalid-assignment] Object of type `T@resolve_block_document_references | dict[str, Any]` is not assignable to `dict[str, Any]`
+ src/prefect/cli/deploy/_core.py:86:21: error[invalid-assignment] Object of type `dict[str, Any] | int | T@resolve_block_document_references | ... omitted 4 union elements` is not assignable to `dict[str, Any]`
- src/prefect/cli/deploy/_core.py:87:21: error[invalid-assignment] Object of type `T@resolve_variables` is not assignable to `dict[str, Any]`
+ src/prefect/cli/deploy/_core.py:87:21: error[invalid-assignment] Object of type `int | T@resolve_variables | float | ... omitted 4 union elements` is not assignable to `dict[str, Any]`
- src/prefect/deployments/steps/core.py:137:38: error[invalid-argument-type] Argument is incorrect: Expected `T@resolve_variables`, found `T@resolve_block_document_references | dict[str, Any]`
+ src/prefect/deployments/steps/core.py:137:38: error[invalid-argument-type] Argument is incorrect: Expected `T@resolve_variables`, found `dict[str, Any] | int | T@resolve_block_document_references | ... omitted 4 union elements`
- src/prefect/utilities/templating.py:320:13: error[invalid-assignment] Invalid subscript assignment with key of type `object` and value of type `T@resolve_block_document_references | dict[str, Any]` on object of type `dict[str, Any]`
+ src/prefect/utilities/templating.py:320:13: error[invalid-assignment] Invalid subscript assignment with key of type `object` and value of type `dict[str, Any] | int | T@resolve_block_document_references | ... omitted 4 union elements` on object of type `dict[str, Any]`
- src/prefect/utilities/templating.py:323:16: error[invalid-return-type] Return type does not match returned value: expected `T@resolve_block_document_references | dict[str, Any]`, found `list[Unknown | T@resolve_block_document_references | dict[str, Any]]`
+ src/prefect/utilities/templating.py:323:16: error[invalid-return-type] Return type does not match returned value: expected `T@resolve_block_document_references | dict[str, Any]`, found `list[Unknown | dict[str, Any] | int | ... omitted 5 union elements]`
- src/prefect/utilities/templating.py:437:16: error[invalid-return-type] Return type does not match returned value: expected `T@resolve_variables`, found `dict[object, Unknown | T@resolve_variables]`
+ src/prefect/utilities/templating.py:437:16: error[invalid-return-type] Return type does not match returned value: expected `T@resolve_variables`, found `dict[object, Unknown | int | T@resolve_variables | ... omitted 5 union elements]`
- src/prefect/utilities/templating.py:442:16: error[invalid-return-type] Return type does not match returned value: expected `T@resolve_variables`, found `list[Unknown | T@resolve_variables]`
+ src/prefect/utilities/templating.py:442:16: error[invalid-return-type] Return type does not match returned value: expected `T@resolve_variables`, found `list[Unknown | int | T@resolve_variables | ... omitted 5 union elements]`
- src/prefect/workers/base.py:232:13: error[invalid-argument-type] Argument is incorrect: Expected `T@resolve_variables`, found `T@resolve_block_document_references | dict[str, Any]`
+ src/prefect/workers/base.py:232:13: error[invalid-argument-type] Argument is incorrect: Expected `T@resolve_variables`, found `dict[str, Any] | int | T@resolve_block_document_references | ... omitted 4 union elements`
- src/prefect/workers/base.py:234:20: error[invalid-argument-type] Argument expression after ** must be a mapping type: Found `T@resolve_variables`
+ src/prefect/workers/base.py:234:20: error[invalid-argument-type] Argument expression after ** must be a mapping type: Found `int | T@resolve_variables | float | ... omitted 4 union elements`

scikit-build-core (https://github.com/scikit-build/scikit-build-core)
- src/scikit_build_core/build/wheel.py:99:20: error[no-matching-overload] No overload of bound method `__init__` matches arguments
- Found 47 diagnostics
+ Found 46 diagnostics

pandas-stubs (https://github.com/pandas-dev/pandas-stubs)
+ tests/frame/test_groupby.py:229:15: error[type-assertion-failure] Type `Series[Any]` does not match asserted type `Series[str | bytes | int | ... omitted 12 union elements]`
+ tests/frame/test_groupby.py:625:15: error[type-assertion-failure] Type `Series[Any]` does not match asserted type `Series[str | bytes | int | ... omitted 12 union elements]`
- Found 4411 diagnostics
+ Found 4413 diagnostics

core (https://github.com/home-assistant/core)
+ homeassistant/util/variance.py:47:12: error[invalid-return-type] Return type does not match returned value: expected `(**_P@ignore_variance) -> _R@ignore_variance`, found `_Wrapped[_P@ignore_variance, int | _R@ignore_variance | float | datetime, _P@ignore_variance, _R@ignore_variance | int | float | datetime]`
- Found 14465 diagnostics
+ Found 14466 diagnostics

Memory usage changes were detected when running on open source projects
prefect (https://github.com/PrefectHQ/prefect)
-     memo fields = ~424MB
+     memo fields = ~445MB

@AlexWaygood AlexWaygood added the ty Multi-file analysis & type inference label Dec 23, 2025
@AlexWaygood
Copy link
Member

The ecosystem results look great! I wouldn't worry about the new diagnostics on rotki or some of the diagnostics where all that's happening is the message changing slightly. We're a bit nondeterministic right now; those just look like our standard flakes.

@Hugo-Polloli Hugo-Polloli force-pushed the typed-dict-as-call-parameter branch from 7c98f57 to 59b330f Compare December 27, 2025 14:12
@Hugo-Polloli Hugo-Polloli marked this pull request as ready for review December 27, 2025 14:23
@Hugo-Polloli
Copy link
Contributor Author

The ecosystem results look great! I wouldn't worry about the new diagnostics on rotki or some of the diagnostics where all that's happening is the message changing slightly. We're a bit nondeterministic right now; those just look like our standard flakes.

Thanks ! I checked too and from my understanding things are looking ok :)

@MichaReiser
Copy link
Member

Thank you for working on this. Almost the entire team is out this week. It may take a few days before someone finds time to review your PR. Happy holidays.

@Hugo-Polloli Hugo-Polloli force-pushed the typed-dict-as-call-parameter branch from 59b330f to b381672 Compare January 7, 2026 08:07
@Hugo-Polloli
Copy link
Contributor Author

Rebased on main as the branch was getting stale, but it looks like everything is still fine with no conflict/regression, so this PR is ready

self.index.try_expression(default_expr).unwrap_or_else(|| {
// Default expressions aren't registered as standalone expressions, so
// synthesize an `Expression` to allow type-context inference.
Expression::new(
Copy link
Member

Choose a reason for hiding this comment

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

Why do we need to perform standalone inference here? You can infer an expression with type context using self.infer_expression.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I started with self.infer_expression, but parameter defaults aren't standalone expressions in the semantic index, so it panics with no entry found for key.
The workaround I found is to synthesize an Expression and run standalone inference in the default's enclosing scope.

Here's the test file I used:

class Foo(TypedDict):
    x: int

def ok(default_x: Foo = {"x": 42}): ... # all good
outer_default_x = 42
def not_ok(default_x: Foo = {"x": outer_default_x}): ...  # panics

I'm unsure that my fix is the correct one, I just know I could not get infer_expression to work in this case, though I agree it would be cleaner if we could make it work here, I'm open to suggestions/learning about things I missed!

Also in the meantime I pushed a diff that extract this custom handling to a file_expression_type_with_context function with comments explaining the reasoning if that can help

Copy link
Member

Choose a reason for hiding this comment

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

parameter defaults aren't standalone expressions in the semantic index

It sounds like you tried infer_standalone_expression, I don't think infer_expression has that panic path?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

oof sorry, "standalone expression" was rly confusing wording on my part! 🫠 I didn't mean I used infer_standalone_expression, I tried infer_expression.

The issue is scope, infer_parameter_definition runs with the function body scope, but the parameter defaults are indexed in the enclosing scope. infer_expression resolves names using the current scope’s ast_ids map. So when defaults reference names from the outer scope (like outer_default_x in my above example), the lookup hits ast_ids.use_id(...) and this is where we panic with no entry found for key.

I hope it makes better sense now?

@Hugo-Polloli Hugo-Polloli force-pushed the typed-dict-as-call-parameter branch from b381672 to 6fe7de9 Compare January 7, 2026 21:00
@ibraheemdev ibraheemdev self-assigned this Jan 16, 2026
.map(|default| self.file_expression_type(default));
let default_expr = default.as_ref();
if let Some(annotation) = parameter.annotation.as_ref() {
let declared_ty = self.file_expression_type(annotation);
Copy link
Member

@ibraheemdev ibraheemdev Jan 21, 2026

Choose a reason for hiding this comment

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

We infer the parameter default as part of the outer scope here. Inferring them multiple times means that the original inferred type will still be displayed, e.g., by the IDE on hover. We should try to infer the value directly with type context in its scope.

I think what this requires is creating a deferred function scope if the function has default value expressions, even if it has type parameters, and then infer default values in infer_function_deferred with the parameter annotations. Note that if there are type parameters, the only thing that should be inferred in infer_function_deferred is the default values, and you should call infer_scope_types on the type-params scope to get the parameter annotation types to use as type context.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hi thank you so much, I completely missed that, I've started working on this and have set this PR as draft for now

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Okay, things are much better now
I hope what I did is what you had in mind! Defaults are now inferred once in the deferred pass with the parameter annotation as type context (for generic functions, we pull annotations from the type‑params scope).

An added benefit of this method is that we replace generic invalid-parameter-default errors by more explicit TypedDict specific errors, I updated the mdtest to reflect that :)

@Hugo-Polloli Hugo-Polloli force-pushed the typed-dict-as-call-parameter branch from 6fe7de9 to 039b775 Compare January 22, 2026 16:10
@Hugo-Polloli Hugo-Polloli marked this pull request as draft January 22, 2026 16:12
@Hugo-Polloli Hugo-Polloli force-pushed the typed-dict-as-call-parameter branch from 039b775 to 48b2666 Compare January 22, 2026 16:26
@Hugo-Polloli Hugo-Polloli force-pushed the typed-dict-as-call-parameter branch from 48b2666 to 63e2872 Compare January 23, 2026 00:42
@Hugo-Polloli Hugo-Polloli marked this pull request as ready for review January 23, 2026 00:55
@ibraheemdev
Copy link
Member

This looks great, thanks!

@ibraheemdev ibraheemdev merged commit 2838bc1 into astral-sh:main Jan 23, 2026
49 checks passed
carljm added a commit that referenced this pull request Jan 30, 2026
* main: (62 commits)
  [`refurb`] Do not add `abc.ABC` if already present (`FURB180`) (#22234)
  [ty] Add a new `assert-type-unspellable-subtype` diagnostic (#22815)
  [ty] Avoid duplicate syntax errors for `await` outside functions (#22826)
  [ty] Fix unary operator false-positive for constrained TypeVars (#22783)
  [ty] Fix binary operator false-positive for constrained TypeVars (#22782)
  [ty] Fix false-positive `unsupported-operator` for "symmetric" TypeVars (#22756)
  [`pydocstyle`] Clarify which quote styles are allowed (`D300`) (#22825)
  [ty] Use distributed versions of AND and OR on constraint sets (#22614)
  [ty] Add support for dict literals and dict() calls as default values for parameters with TypedDict types (#22161)
  Document `-` stdin convention in CLI help text (#22817)
  [ty] Make `infer_subscript_expression_types` a method on `Type` (#22731)
  [ty] Simplify `OverloadLiteral::spans` and `OverloadLiteral::parameter_span` (#22823)
  [ty] Require both `*args` and `**kwargs` when calling a `ParamSpec` callable (#22820)
  [ty] Handle tagged errors in conformance (#22746)
  Add `--color` cli option to force colored output (#22806)
  Identify notebooks by LSP didOpen instead of `.ipynb` file extension (#22810)
  [ty] Fix docstring rendering for literal blocks after doctests (#22676)
  [ty] Update salsa to fix out-of-order query validation (#22498)
  [ty] Inline cycle initial and recovery functions (#22814)
  [ty] Pass the generic context through the decorator (#22544)
  ...
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.

Support dict literals and dict() calls as default values for parameters annotated with TypedDict types

4 participants