Skip to content

Dict-literal arguments don't try all members of a union of dict[...] types #4913

Description

@MarcoGorelli

Describe the Bug

from typing import Generic, Hashable, TypeVar, reveal_type

S1 = TypeVar("S1")
K = TypeVar("K", bound=Hashable)

class Box(Generic[S1]):
    def __init__(self, data: dict[str, list[int]] | dict[K, S1]) -> None: ...

reveal_type(Box({"a": 1, "b": 2}))
ERROR sandbox.py:9:23-24: `Literal[1]` is not assignable to dict value type `list[int]` [[bad-assignment](https://pyrefly.org/en/docs/error-kinds/#bad-assignment)]
ERROR sandbox.py:9:31-32: `Literal[2]` is not assignable to dict value type `list[int]` [[bad-assignment](https://pyrefly.org/en/docs/error-kinds/#bad-assignment)]

I think this should pass the dict[K, S1] part?

If we write it as

d: dict[str, int] = {"a": 1, "b": 2}
reveal_type(Box(d))  # same dict, via a pre-typed variable

then Pyrefly accepts it

Spotted in pandas-stubs

Sandbox Link

https://pyrefly.org/sandbox/?project=v2.pZXNqsIwEIVfZWgXrVAbepfC3bgSuhG6tF1EU7SbRoyKIr67Zya2WBVB3JX8nEky53z9Apk9EmfabTw7eoA9MajIcNj7ZFwGRVYGaEk-GMzLIKGlPbTmvxOU7vqQTu0pvhdcFFnV-fJNWhkKEzKAJ-c88YmHEaoKkZHhHESDRO9FStOUKz0cOuZ6lzLQsBtlCR56yV9_19GIKBQZCAv1JaaPBbkWbvZu92sN4wUdnC4SoE2jScO79ZgXGeRt1_BjSI-fHBbS3A9AQJoFtsKa6wMIDsvhViHgXdNmv9-6iVLddrtbq7pVxq6cGuxQxGHSR93If2EoR1YS4SD7i7Uj7-zok7Ej9rWsGOv2jEWCHoyLOCY-mvgG

(Only applicable for extension issues) IDE Information

No response

Activity

  1. added theissue type on Sep 27, 2026
  2. Divyansh151005 commented on Oct 28, 2026

    @Divyansh151005
    Contributor

    #claim

    I'd like to work on this. I traced it to dict-literal hint decomposition:

    • The trigger is a union member whose key and value are both type variables (dict[K, S1]); the bound on K doesn't matter. dict[K, int] and dict[str, S1] already work, and an unbounded dict[str, list[int]] | dict[KU, S1] fails the same way.
    • In dict_items_infer (and the DictComp path), the decompose closure returns None when both the key and value hints are unresolved. infer_with_decomposed_hint treats None as "this member isn't a dict", so dict[K, S1] is skipped before inference runs. We then fall back to dict[str, list[int]], which matched with errors.
    • Sets don't hit this because set elements use best-effort hint coercion, so the erroring fallback is never recorded.

    Plan: have decompose_dict return None only when the hint can't be a dict. A dict hint with placeholder vars should still be decomposed even when the key and value hints are unresolved, since inferring against it is what pins those vars. Uninformative var-free members such as object keep being skipped, so a more informative sibling like dict[str, float] still wins. I'll add regression tests for the issue example (both union orders), the dict-comprehension path, and the unchanged cases.

  3. github-actions commented on Oct 28, 2026

    @github-actions

    @Divyansh151005 you've claimed this issue — it's now assigned to you. Thanks for picking it up! 🎉

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

Metadata

Metadata

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions