pyrefly icon indicating copy to clipboard operation
pyrefly copied to clipboard

[Bug][CLI]: `dict` -> `TypedDict` narrowing fails in `Union` context

Open mrsobakin opened this issue 6 months ago • 3 comments

Describe the Bug

Thank you for your work!

When assigning a dictionary literal to a variable typed as Union[SomeTypedDict, ...], Pyrefly raises an error, even though the dictoronary can be narrowed to SomeTypedDict.

Example:

from typing import TypedDict

class Foo(TypedDict):
    key: str

# ok
bar: Foo = {
    "key": "",
}

# error here: dict[str, str] is not assignable to str | TypedDict[Foo]
baz: Foo | str = {
    "key": "",
}

Codebase

Sandbox: https://pyrefly.org/sandbox/?code=GYJw9gtgBALgngBwJYDsDmUkQWEMoAqiApgCYAiSAxjAFC1UA2AhgM6tQBiYYAFEQjKUaASgBctKFKgBrYnDFRWMEPQDEUMDNoAjZiEXcwUALxQA3pOkAiOXGuLr1gDS0AvuqjEQ4EFAAW3sSKpNQwANrKIM5KKgC6mBwoYPhsrEhoKMw6jMSwxlFQAD6EJBRh4UZxuswAXoY8xbF%20ZpbSULbyDh0u7kA

Other Attempts

No response

mrsobakin avatar May 06 '25 09:05 mrsobakin

So I think the root cause of this is that our contextual subtyping doesn't work super well with unions, but due to the way TypedDicts are handled, it never works for TypedDicts in unions while it can sometimes work for other types.

We're definitely going to fix this but we may not get to it before PyCon

yangdanny97 avatar May 09 '25 12:05 yangdanny97

This is not contextual subtyping, it's due to the subset relation.

from typing import TypedDict, Literal, Any

class Foo(TypedDict):
    key1: str
    key2: str

bar: Foo = {"key1": "", "key2": ""}

def f1(x: dict[Any, Any]): pass
def f2(x: dict[str, str]): pass
def f3(x: dict[str, bool]): pass

f1(bar) # should work
f2(bar) # should work
f3(bar) # should fail

ndmitchell avatar May 18 '25 20:05 ndmitchell

Actually, I think this is more nuanced. My reproduction is based around the same subset piece, but we really need subset-but-not-mutable, I think. I don't really understand quite enough, so will talk to someone.

ndmitchell avatar May 18 '25 20:05 ndmitchell

@ndmitchell it seems to me that the original example is about contextual typing. Your reproduction case looks wrong to me, because we should never be able to pass a TypedDict into a position that expects a dict type, so all of the calls should fail, including f1 and f2.

I'm going to take a look at the contextual typing part of this.

samwgoldman avatar Jul 02 '25 18:07 samwgoldman

Discussed with Sam, I'm going to look into this.

rchen152 avatar Aug 01 '25 17:08 rchen152