[ty] Infer dict(TypedDict) as dict[str, object]#24852
Conversation
Typing conformance resultsNo changes detected ✅Current numbersThe percentage of diagnostics emitted that were expected errors held steady at 89.36%. The percentage of expected errors that received a diagnostic held steady at 85.49%. The number of fully passing files held steady at 88/134. |
Memory usage reportMemory usage unchanged ✅ |
|
| Lint rule | Added | Removed | Changed |
|---|---|---|---|
no-matching-overload |
0 | 23 | 0 |
invalid-argument-type |
0 | 3 | 0 |
| Total | 0 | 26 | 0 |
Raw diff (26 changes)
aioredis (https://github.com/aio-libs/aioredis)
- aioredis/connection.py:1301:9 error[no-matching-overload] No overload of bound method `MutableMapping.update` matches arguments
core (https://github.com/home-assistant/core)
- homeassistant/components/weather/__init__.py:701:46 error[no-matching-overload] No overload of `dict.__init__` matches arguments
- homeassistant/components/auth/mfa_setup_flow.py:146:16 error[no-matching-overload] No overload of `dict.__init__` matches arguments
- homeassistant/components/fronius/config_flow.py:117:53 error[no-matching-overload] No overload of `dict.__init__` matches arguments
- homeassistant/components/mqtt/config_flow.py:3777:65 error[no-matching-overload] No overload of `dict.__init__` matches arguments
- homeassistant/components/mqtt/config_flow.py:4891:13 error[no-matching-overload] No overload of `dict.__init__` matches arguments
- homeassistant/components/mqtt/config_flow.py:4996:13 error[no-matching-overload] No overload of bound method `MutableMapping.update` matches arguments
- homeassistant/components/mqtt/config_flow.py:4997:13 error[no-matching-overload] No overload of bound method `MutableMapping.update` matches arguments
- homeassistant/components/mqtt/config_flow.py:5035:9 error[no-matching-overload] No overload of bound method `MutableMapping.update` matches arguments
- homeassistant/components/mqtt/config_flow.py:5047:13 error[no-matching-overload] No overload of bound method `MutableMapping.update` matches arguments
- homeassistant/components/mqtt/config_flow.py:5048:13 error[no-matching-overload] No overload of bound method `MutableMapping.update` matches arguments
- homeassistant/components/mqtt/entity.py:386:17 error[no-matching-overload] No overload of bound method `MutableMapping.update` matches arguments
- homeassistant/components/mqtt/entity.py:387:17 error[no-matching-overload] No overload of bound method `MutableMapping.update` matches arguments
cwltool (https://github.com/common-workflow-language/cwltool)
- cwltool/cwlprov/ro.py:359:21 error[no-matching-overload] No overload of bound method `MutableMapping.update` matches arguments
dd-trace-py (https://github.com/DataDog/dd-trace-py)
- ddtrace/llmobs/_prompt_optimization.py:316:49 error[no-matching-overload] No overload of `dict.__init__` matches arguments
discord.py (https://github.com/Rapptz/discord.py)
- discord/client.py:2717:93 error[invalid-argument-type] Argument to bound method `str.format_map` is incorrect: Expected `_FormatMapMapping`, found `TextChannel | NewsChannel | VoiceChannel | ... omitted 7 union elements`
- discord/guild.py:2626:93 error[invalid-argument-type] Argument to bound method `str.format_map` is incorrect: Expected `_FormatMapMapping`, found `TextChannel | NewsChannel | VoiceChannel | ... omitted 7 union elements`
- discord/interactions.py:260:92 error[invalid-argument-type] Argument to bound method `str.format_map` is incorrect: Expected `_FormatMapMapping`, found `TextChannel | NewsChannel | VoiceChannel | ... omitted 7 union elements`
hydpy (https://github.com/hydpy-dev/hydpy)
- hydpy/models/rconc/rconc_control.py:249:23 error[no-matching-overload] No overload of `dict.__init__` matches arguments
packaging (https://github.com/pypa/packaging)
- src/packaging/pylock.py:780:13 error[no-matching-overload] No overload of `dict.__init__` matches arguments
pip (https://github.com/pypa/pip)
- src/pip/_vendor/packaging/pylock.py:775:13 error[no-matching-overload] No overload of `dict.__init__` matches arguments
poetry (https://github.com/python-poetry/poetry)
- src/poetry/utils/authenticator.py:233:9 error[no-matching-overload] No overload of bound method `MutableMapping.update` matches arguments
pydantic (https://github.com/pydantic/pydantic)
- pydantic/_internal/_mock_val_ser.py:41:16 error[no-matching-overload] No overload matches arguments
scikit-learn (https://github.com/scikit-learn/scikit-learn)
- sklearn/externals/array_api_compat/dask/array/_info.py:378:17 error[no-matching-overload] No overload of bound method `MutableMapping.update` matches arguments
scipy (https://github.com/scipy/scipy)
- subprojects/array_api_compat/array_api_compat/array_api_compat/dask/array/_info.py:378:17 error[no-matching-overload] No overload of bound method `MutableMapping.update` matches arguments
zulip (https://github.com/zulip/zulip)
- zerver/views/auth.py:379:30 error[no-matching-overload] No overload of `dict.__init__` matches arguments
Indeed it wouldn't haha |
|
It looks like there are some false positives from cases in which we're relying on the inferred rather than the annotated type, e.g.: forecast_entry: dict[str, Any] = dict(_forecast_entry)We then treat entries in this |
That'll be fixed by the PR I'm working on for astral-sh/ty#136 |
|
Figured as much, thanks! |
carljm
left a comment
There was a problem hiding this comment.
Hmm, I am not totally convinced that special-casing is the right way out here, though maybe in the end it will be. (Especially since IIRC we have other diffs in the pipeline that do special-cased handling of even more dict(...) signatures?)
The normal (non-special-cased) inference path for dict.__init__ matches a single positional argument against the SupportsKeysAndGetItem protocol, and infers key and value types from that. It looks like if we completed our TypedDict __getitem__ overloads with a final overload for (str) -> object (which is technically correct for a non-closed TypedDict -- but would mean we no longer error on subscripting a non-closed TypedDict with unexpected key names, unless we added a special-cased rule to check for that) then the SupportsKeysAndGetIem assignability check would infer the right types here.
Throwing this into the playground or multiplay might clarify what I mean:
from typing import TypedDict, Iterable, Literal, overload
class TD(TypedDict):
x: int
y: str
class ManualBad:
def keys(self) -> Iterable[str]: ...
@overload
def __getitem__(self, key: Literal["x"]) -> int: ...
@overload
def __getitem__(self, key: Literal["y"]) -> str: ...
def __getitem__(self, key: str) -> object: ...
class Manual:
def keys(self) -> Iterable[str]: ...
@overload
def __getitem__(self, key: Literal["x"]) -> int: ...
@overload
def __getitem__(self, key: Literal["y"]) -> str: ...
@overload
def __getitem__(self, key: str) -> object: ...
def __getitem__(self, key: str) -> object: ...
def takes_dict(value: dict[str, object]) -> None: ...
def takes_kwargs(**kwargs: object) -> None: ...
def _(data: TD, bad: ManualBad, man: Manual):
reveal_type(dict(bad))
takes_dict(dict(bad))
reveal_type(dict(man))
takes_dict(dict(man))
reveal_type(data.__getitem__)
reveal_type(dict(data))
takes_dict(dict(data))|
|
||
| // Probe silently so non-TypedDict arguments fall back to normal call binding without | ||
| // duplicate diagnostics; merge the inference only after accepting the fast path. | ||
| let mut speculative_builder = self.speculate(); |
There was a problem hiding this comment.
Previously I think we've always used speculative building when we really need to infer the same thing with different type contexts. I don't love starting down this slope of using it not because we need different type context, but just because it's too much of a pain to reorganize things to avoid double-inference. It's not super likely, but certainly possible, that someone wraps dict(...) around a sizable expression that's actually costly to double infer.
|
Your summary makes sense to me, and I pushed up the change, but I haven't reviewed the ecosystem diff yet. |
This comment was marked as outdated.
This comment was marked as outdated.
ccfa5cb to
f9eb452
Compare
|
One downside (?) is that We do still emit a diagnostic when subscripting with invalid keys via this special case. |
f9eb452 to
0ece324
Compare
e1e39d6 to
fd52ddf
Compare
fd52ddf to
7f73cbd
Compare
7f73cbd to
4f8606b
Compare
13122b1 to
b985e26
Compare
b985e26 to
f1fcc45
Compare
## Summary
Given:
```python
from typing import TypedDict
class TD(TypedDict):
x: int
y: str
def _(data: TD):
reveal_type(dict(data))
```
We now reveal `dict[str, object]`.
We could consider using a narrower value type (e.g., infer `dict[str,
int]` if all values are `int`), though Mypy and Pyright don't do that (I
suppose it wouldn't be sound for open TypedDicts?).
Closes astral-sh/ty#3340.
## Summary
Given:
```python
from typing import TypedDict
class TD(TypedDict):
x: int
y: str
def _(data: TD):
reveal_type(dict(data))
```
We now reveal `dict[str, object]`.
We could consider using a narrower value type (e.g., infer `dict[str,
int]` if all values are `int`), though Mypy and Pyright don't do that (I
suppose it wouldn't be sound for open TypedDicts?).
Closes astral-sh/ty#3340.
Summary
Given:
We now reveal
dict[str, object].We could consider using a narrower value type (e.g., infer
dict[str, int]if all values areint), though Mypy and Pyright don't do that (I suppose it wouldn't be sound for open TypedDicts?).Closes astral-sh/ty#3340.