Skip to content

[ty] pass type context to sequence literals in binary operations#24197

Merged
oconnor663 merged 3 commits intomainfrom
jack/list_mul_type_context
Apr 2, 2026
Merged

[ty] pass type context to sequence literals in binary operations#24197
oconnor663 merged 3 commits intomainfrom
jack/list_mul_type_context

Conversation

@oconnor663
Copy link
Copy Markdown
Contributor

@oconnor663 oconnor663 commented Mar 26, 2026

Fixes astral-sh/ty#3002.

This is a quick fix for this special case. A more general solution will be passing type context through generic method calls, with binary operations like these handled via their dunder methods.

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

astral-sh-bot bot commented Mar 26, 2026

Typing conformance results

No changes detected ✅

Current numbers
The percentage of diagnostics emitted that were expected errors held steady at 86.79%. The percentage of expected errors that received a diagnostic held steady at 81.72%. The number of fully passing files held steady at 70/132.

@astral-sh-bot
Copy link
Copy Markdown

astral-sh-bot bot commented Mar 26, 2026

Memory usage report

Summary

Project Old New Diff Outcome
prefect 716.78MB 716.82MB +0.01% (47.79kB)
sphinx 264.61MB 264.62MB +0.00% (10.59kB)
trio 117.73MB 117.73MB +0.00% (828.00B)
flake8 47.99MB 47.99MB +0.00% (608.00B)

Significant changes

Click to expand detailed breakdown

prefect

Name Old New Diff Outcome
infer_definition_types 90.05MB 90.06MB +0.01% (12.70kB)
infer_expression_types_impl 62.03MB 62.04MB +0.01% (9.06kB)
infer_scope_types_impl 54.27MB 54.27MB +0.01% (7.59kB)
StaticClassLiteral<'db>::try_mro_ 5.87MB 5.88MB +0.10% (6.01kB)
infer_expression_type_impl 13.90MB 13.90MB +0.03% (4.58kB)
is_redundant_with_impl::interned_arguments 5.43MB 5.43MB -0.05% (2.66kB)
all_narrowing_constraints_for_expression 7.16MB 7.16MB +0.03% (2.19kB)
UnionType<'db>::from_two_elements_ 5.25MB 5.25MB -0.04% (2.08kB)
Specialization 2.50MB 2.50MB +0.05% (1.39kB)
StaticClassLiteral<'db>::try_mro_::interned_arguments 1.37MB 1.37MB +0.09% (1.20kB)
is_redundant_with_impl 5.52MB 5.52MB -0.02% (1.20kB)
all_negative_narrowing_constraints_for_expression 2.63MB 2.63MB +0.04% (1.20kB)
Type<'db>::class_member_with_policy_ 17.77MB 17.77MB +0.01% (1.04kB)
Type<'db>::try_call_dunder_get_ 10.78MB 10.78MB +0.01% (1.04kB)
FunctionType 8.72MB 8.72MB +0.01% (1.00kB)
... 25 more

sphinx

Name Old New Diff Outcome
infer_scope_types_impl 15.50MB 15.51MB +0.05% (7.43kB)
infer_expression_types_impl 21.51MB 21.51MB +0.01% (2.21kB)
infer_definition_types 24.00MB 24.00MB +0.00% (648.00B)
ScopeWithContext 9.77kB 10.08kB +3.20% (320.00B)

trio

Name Old New Diff Outcome
infer_expression_types_impl 7.09MB 7.09MB +0.01% (456.00B)
infer_definition_types 7.73MB 7.73MB +0.00% (372.00B)

flake8

Name Old New Diff Outcome
infer_scope_types_impl 998.03kB 998.51kB +0.05% (492.00B)
ClassType<'db>::nearest_disjoint_base_ 2.72kB 2.84kB +4.16% (116.00B)

@astral-sh-bot
Copy link
Copy Markdown

astral-sh-bot bot commented Mar 26, 2026

ecosystem-analyzer results

Lint rule Added Removed Changed
invalid-assignment 0 27 3
unsupported-operator 0 10 0
invalid-argument-type 0 8 0
invalid-return-type 0 3 1
no-matching-overload 0 1 0
not-iterable 0 1 0
Total 0 50 4
Raw diff (54 changes)
core (https://github.com/home-assistant/core)
- homeassistant/components/osoenergy/water_heater.py:163:36 error[invalid-assignment] Object of type `list[int | float]` is not assignable to `list[JsonValueType]`

dedupe (https://github.com/dedupeio/dedupe)
- dedupe/labeler.py:451:26 error[invalid-assignment] Object of type `list[int]` is not assignable to `list[Literal[0, 1]]`
- dedupe/labeler.py:487:26 error[invalid-assignment] Object of type `list[int]` is not assignable to `list[Literal[0, 1]]`

egglog-python (https://github.com/egraphs-good/egglog-python)
- python/egglog/pretty.py:136:28 error[invalid-assignment] Object of type `list[UnboundVarDecl]` is not assignable to `list[DummyDecl | UnboundVarDecl | LetRefDecl | ... omitted 6 union elements]`
- python/egglog/pretty.py:149:44 error[invalid-assignment] Object of type `list[UnboundVarDecl]` is not assignable to `list[DummyDecl | UnboundVarDecl | LetRefDecl | ... omitted 6 union elements]`

ignite (https://github.com/pytorch/ignite)
- ignite/handlers/param_scheduler.py:1674:22 error[invalid-assignment] Object of type `list[int]` is not assignable to `int | float | list[int | float]`
- ignite/handlers/param_scheduler.py:1675:13 error[invalid-assignment] Cannot assign to a subscript on an object of type `float`
- ignite/handlers/param_scheduler.py:1675:13 error[invalid-assignment] Cannot assign to a subscript on an object of type `int`

jax (https://github.com/google/jax)
- jax/experimental/jet.py:459:35 error[invalid-argument-type] Argument to function `square` is incorrect: Expected `Array | ndarray[tuple[Any, ...], dtype[Any]] | numpy.bool[builtins.bool] | ... omitted 4 union elements`, found `None | Unknown | Array`
- jax/experimental/jet.py:460:33 error[invalid-argument-type] Argument to function `exp` is incorrect: Expected `Array | ndarray[tuple[Any, ...], dtype[Any]] | numpy.bool[builtins.bool] | ... omitted 4 union elements`, found `None | Unknown | Array`
- jax/experimental/jet.py:465:16 error[unsupported-operator] Operator `*` is not supported between objects of type `int` and `None | Unknown | Array`
- jax/experimental/jet.py:470:22 error[unsupported-operator] Operator `*` is not supported between two objects of type `None | Unknown | Array`
- jax/experimental/jet.py:473:22 error[unsupported-operator] Operator `*` is not supported between objects of type `int` and `None | Unknown | Array`
- jax/experimental/jet.py:480:14 error[unsupported-operator] Operator `*` is not supported between objects of type `int` and `None | Unknown | Array`
- jax/experimental/jet.py:494:16 error[unsupported-operator] Operator `*` is not supported between objects of type `int` and `None | Unknown | Array`
- jax/experimental/jet.py:536:27 error[unsupported-operator] Operator `-` is not supported between objects of type `Literal[1]` and `None | Unknown | Array`
- jax/experimental/jet.py:539:13 error[unsupported-operator] Operator `-` is not supported between objects of type `Literal[1]` and `None | Unknown | Array`
- jax/experimental/jet.py:539:36 error[unsupported-operator] Operator `*` is not supported between two objects of type `None | Unknown | Array`
- jax/experimental/jet.py:563:16 error[unsupported-operator] Operator `*` is not supported between objects of type `int` and `None | Unknown | Array`
- jax/_src/export/_export.py:1294:46 error[invalid-assignment] Object of type `list[UnspecifiedValue]` is not assignable to `list[Sharding | UnspecifiedValue]`
- jax/_src/numpy/lax_numpy.py:2206:34 error[invalid-assignment] Object of type `list[int]` is not assignable to `list[Array | ndarray[tuple[Any, ...], dtype[Any]] | numpy.bool[builtins.bool] | ... omitted 4 union elements]`
- jax/_src/numpy/vectorize.py:282:25 error[invalid-assignment] Object of type `list[tuple[()]]` is not assignable to `list[tuple[str, ...]]`
- jax/_src/shard_map.py:542:44 error[invalid-assignment] Object of type `list[NoFail]` is not assignable to `list[ShapedArray | NoFail]`

meson (https://github.com/mesonbuild/meson)
- mesonbuild/backend/ninjabackend.py:2459:27 error[invalid-assignment] Object of type `list[str] | list[Unknown]` is not assignable to `list[str | NinjaCommandArg]`
- mesonbuild/backend/ninjabackend.py:1421:46 error[invalid-argument-type] Argument to bound method `__init__` is incorrect: Expected `list[NinjaCommandArg | str]`, found `list[str]`

mypy (https://github.com/python/mypy)
- mypyc/ir/rtypes.py:1260:51 error[invalid-argument-type] Argument to bound method `__init__` is incorrect: Expected `list[RType]`, found `list[RStruct]`
- mypyc/primitives/generic_ops.py:126:9 error[invalid-argument-type] Argument to function `function_op` is incorrect: Expected `list[RType]`, found `list[RPrimitive]`
- mypy/checker.py:5760:31 error[invalid-argument-type] Argument to bound method `check_method_call_by_name` is incorrect: Expected `list[Expression]`, found `list[TempNode]`
- mypy/checker.py:5776:30 error[invalid-argument-type] Argument to bound method `check_method_call_by_name` is incorrect: Expected `list[Expression]`, found `list[TempNode]`
- mypy/checkexpr.py:5068:28 error[invalid-return-type] Return type does not match returned value: expected `list[Type]`, found `list[AnyType]`
- mypy/fastparse.py:987:29 error[invalid-assignment] Object of type `list[AnyType]` is not assignable to `list[Type | None]`

pandas (https://github.com/pandas-dev/pandas)
- pandas/tests/arrays/masked/test_arithmetic.py:16:1 error[unsupported-operator] Operator `+=` is not supported between objects of type `list[int]` and `list[int | float]`
- pandas/core/indexes/multi.py:592:22 error[invalid-assignment] Object of type `list[list[Unknown]]` is not assignable to `list[Sequence[Hashable]]`
- pandas/core/indexing.py:1838:63 error[invalid-assignment] Object of type `list[slice[Any, Any, Any]]` is not assignable to `list[slice[Any, Any, Any] | ndarray[tuple[Any, ...], dtype[signedinteger[_64Bit]]]]`
- pandas/core/internals/managers.py:1262:16 error[invalid-return-type] Return type does not match returned value: expected `list[ndarray[tuple[Any, ...], dtype[Any]]]`, found `list[None | Unknown]`
+ pandas/core/internals/managers.py:1262:16 error[invalid-return-type] Return type does not match returned value: expected `list[ndarray[tuple[Any, ...], dtype[Any]]]`, found `list[ndarray[tuple[Any, ...], dtype[Any]] | None]`
- pandas/io/excel/_odfreader.py:158:30 error[invalid-argument-type] Argument to bound method `extend` is incorrect: Expected `Iterable[list[str | bytes | date | ... omitted 11 union elements]]`, found `list[list[str]]`
- pandas/io/formats/html.py:347:23 error[invalid-assignment] Object of type `list[str]` is not assignable to `list[Hashable]`
- pandas/io/formats/html.py:380:19 error[invalid-assignment] Object of type `list[str]` is not assignable to `list[Hashable]`
- pandas/io/pytables.py:5166:39 error[invalid-assignment] Object of type `list[slice[Any, Any, Any]]` is not assignable to `list[slice[Any, Any, Any] | Index]`
- pandas/io/sql.py:1049:39 error[invalid-assignment] Object of type `list[None | Unknown]` is not assignable to `list[ndarray[tuple[Any, ...], dtype[Any]]]`
+ pandas/io/sql.py:1049:39 error[invalid-assignment] Object of type `list[ndarray[tuple[Any, ...], dtype[Any]] | None]` is not assignable to `list[ndarray[tuple[Any, ...], dtype[Any]]]`

prefect (https://github.com/PrefectHQ/prefect)
- src/prefect/futures.py:517:28 error[invalid-assignment] Object of type `list[None | Unknown]` is not assignable to `list[R@PrefectFutureList]`
+ src/prefect/futures.py:517:28 error[invalid-assignment] Object of type `list[R@PrefectFutureList | None]` is not assignable to `list[R@PrefectFutureList]`

rotki (https://github.com/rotki/rotki)
- rotkehlchen/chain/evm/decoding/quickswap/v3/decoder.py:176:16 error[invalid-return-type] Return type does not match returned value: expected `dict[str, list[tuple[int, (...) -> Unknown]]]`, found `dict[str, list[tuple[int, (...) -> Unknown]] | list[tuple[int, bound method Self@post_decoding_rules._v3_router_post_decoding(transaction: EvmTransaction, decoded_events: list[EvmEvent], all_logs: list[EvmTxReceiptLog]) -> list[EvmEvent]]]]`
- rotkehlchen/chain/evm/decoding/quickswap/v4/decoder.py:73:16 error[invalid-return-type] Return type does not match returned value: expected `dict[str, list[tuple[int, (...) -> Unknown]]]`, found `dict[str, list[tuple[int, (...) -> Unknown]] | list[tuple[int, bound method Self@post_decoding_rules._router_post_decoding(transaction: EvmTransaction, decoded_events: list[EvmEvent], all_logs: list[EvmTxReceiptLog]) -> list[EvmEvent]]]]`

scikit-learn (https://github.com/scikit-learn/scikit-learn)
- sklearn/neural_network/tests/test_mlp.py:102:5 error[invalid-assignment] Invalid subscript assignment with key of type `Literal[0]` and value of type `ndarray[tuple[Any, ...], dtype[Any]]` on object of type `list[int]`
- sklearn/neural_network/tests/test_mlp.py:103:5 error[invalid-assignment] Invalid subscript assignment with key of type `Literal[1]` and value of type `ndarray[tuple[Any, ...], dtype[Any]]` on object of type `list[int]`
- sklearn/neural_network/tests/test_mlp.py:104:5 error[invalid-assignment] Invalid subscript assignment with key of type `Literal[0]` and value of type `ndarray[tuple[Any, ...], dtype[Any]]` on object of type `list[int]`
- sklearn/neural_network/tests/test_mlp.py:105:5 error[invalid-assignment] Invalid subscript assignment with key of type `Literal[1]` and value of type `ndarray[tuple[Any, ...], dtype[Any]]` on object of type `list[int]`

sockeye (https://github.com/awslabs/sockeye)
- sockeye/model.py:812:23 error[invalid-assignment] Object of type `list[None | Unknown]` is not assignable to `list[int] | None`
+ sockeye/model.py:812:23 error[invalid-assignment] Object of type `list[int | None]` is not assignable to `list[int] | None`

sympy (https://github.com/sympy/sympy)
- sympy/polys/puiseux.py:514:32 error[invalid-assignment] Object of type `Unknown | list[int]` is not assignable to `list[MPQ | int]`
- sympy/matrices/matrixbase.py:3931:46 error[invalid-assignment] Object of type `list[int]` is not assignable to `list[int | MatrixBase]`
- sympy/matrices/matrixbase.py:4878:31 error[invalid-assignment] Object of type `list[list[Expr] | list[Zero]]` is not assignable to `list[list[Expr]]`

vision (https://github.com/pytorch/vision)
- torchvision/transforms/v2/_container.py:141:17 error[invalid-assignment] Object of type `list[int]` is not assignable to `list[int | float] | None`
- torchvision/transforms/v2/_container.py:148:17 error[no-matching-overload] No overload of function `sum` matches arguments
- torchvision/transforms/v2/_container.py:149:44 error[not-iterable] Object of type `list[int | float] | None` may not be iterable

Full report with detailed diff (timing results)

@oconnor663 oconnor663 force-pushed the jack/list_mul_type_context branch 2 times, most recently from 1a6348c to d3e77ff Compare March 26, 2026 19:47
@oconnor663 oconnor663 changed the title [ty] pass type context to sequence repetitions [ty] pass type context to both sides of binary operations Mar 26, 2026
@oconnor663
Copy link
Copy Markdown
Contributor Author

The added diagnostic in mesonbuild/dependencies/cuda.py looks like a true positive. The variable in question comes from iterating over a list that's constructed by + concatenation, which gets type context after this change.

@oconnor663 oconnor663 force-pushed the jack/list_mul_type_context branch from c9e1e58 to 4aff95f Compare April 1, 2026 03:35
@oconnor663 oconnor663 changed the title [ty] pass type context to both sides of binary operations [ty] pass type context to sequence literals in binary operations Apr 1, 2026
@oconnor663 oconnor663 marked this pull request as ready for review April 1, 2026 03:42
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4aff95fbba

ℹ️ About Codex in GitHub

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

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

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines 155 to 157
return BinaryExpressionOperandTypes::Inferred(
self.infer_expression(left, TypeContext::default()),
self.infer_expression(left, operand_tcx(left)),
right_ty,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Remove TypedDict context in | fallback literal inference

When the left operand is a dict literal and the TypedDict fast-path rejects it, this fallback now re-infers the left literal with operand_tcx(left), which may be a TypedDict target context. That reintroduces the same spurious missing-typed-dict-key/invalid-key diagnostics this branch is meant to avoid (the comment still says it should re-infer without TypedDict context). In cases like assigning a merged dict expression to a TypedDict, the left partial literal can now emit premature key errors purely because of the outer context.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

@oconnor663 oconnor663 Apr 2, 2026

Choose a reason for hiding this comment

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

This is confusing, and I'm not sure what we actually want to have happen here. The situation is:

x: MyTypedDict = <dict_literal> | <expression2>

If inferring expression2 with the default context yields a TypedDict type, then we speculatively check whether dict_literal matches that type. If it doesn't match, then currently on the main branch we fall back to inferring dict_literal also with the default context. But it seems like it might be more correct to infer dict_literal with the inherited MyTypedDict context? In fact it seems like it might be more correct to infer both sides that way.

On the other hand, do we care to support a wacky situation like this:

class MyTypedDict(TypedDict):
    a: int
    b: str

x: MyTypedDict = {"a": 42} | {"b": "hello"}

The problem there is that the unioned value is a valid MyTypedDict, but the two halves of it aren't, so passing MyTypedDict as type context to either side will result in errors. On the other hand, this is pretty convoluted, and we don't support it today in any case. @ibraheemdev what's your instinct here?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

But it seems like it might be more correct to infer dict_literal with the inherited MyTypedDict context?

If we failed to perform the speculative typed dict update, dict_literal is not even a valid subset of MyTypedDict, and so inferring it as MyTypedDict directly would likely lead to a lot of missing-key-errors that don't seem very useful.

On the other hand, do we care to support a wacky situation like this:

pyright and mypy doesn't seem to support this either, so it seems fine to ignore it. Maybe we should just ignore TypedDict context entirely outside of the speculative updates to avoid the extra errors, if there is no way to produce a valid TypedDict otherwise (unless there is a case I am missing)?

@carljm carljm removed their request for review April 1, 2026 14:27
Copy link
Copy Markdown
Member

@ibraheemdev ibraheemdev left a comment

Choose a reason for hiding this comment

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

As discussed, this is not entirely sound, but seems to work well enough in the common case. It might be worth adding a TODO test where eagerly passing the type context can break, e.g.,

class X:
    def __add__(self, _: list[int]) -> list[int | str]:
        return []

x: list[int | str] = X() + [1]

which type-checks fine on main, but errors on this PR.

@oconnor663 oconnor663 merged commit d80c46e into main Apr 2, 2026
52 checks passed
@oconnor663 oconnor663 deleted the jack/list_mul_type_context branch April 2, 2026 19:47
carljm added a commit that referenced this pull request Apr 3, 2026
* main:
  Document adding fixes in CONTRIBUTING.md (#24393)
  Sort formatter diagnostics in snapshots (#24375)
  [`pyupgrade`] Fix panic caused by handling of octals in `UP012` (#24390)
  Upgrade to nix v0.31.2 (#24385)
  Strip form feeds from indent passed to `dedent_to` (#24381)
  add recent move of the `deferred` submodule to `.git-blame-ignore-revs` (#24379)
  [ty] Fix extra_items TypedDict tests (#24367)
  [ty] Use `infer_type_expression` for validating PEP-613 type aliases (#24370)
  [`flake8-simplify`] Make the fix for `collapsible-if` (`SIM102`) safe in `preview` (#24371)
  [ty] Validate TypedDict fields when subclassing (#24338)
  [ty] pass type context to sequence literals in binary operations (#24197)
  Add release environment to notify-dependents job (#24372)
  Bump 0.15.9 (#24369)
  [ty] Move the `deferred` submodule inside `infer/builder` (#24368)
  [ty] Infer the `extra_items` keyword argument to class-based TypedDicts as an annotation expression (#24362)
  [ty] Validate type qualifiers in functional TypedDict fields and the `extra_items` keyword to functional TypedDicts (#24360)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ecosystem-analyzer ty Multi-file analysis & type inference

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[None] * int not widened to match expected list type

2 participants