Skip to content

stdlib/2and3/builtins: make dict.fromkeys(iterable) have None values, not Any - #3799

Closed
bluetech wants to merge 1 commit into
python:masterfrom
bluetech:dict-fromkeys-none
Closed

bluetech wants to merge 1 commit into
python:masterfrom
bluetech:dict-fromkeys-none

Conversation

@bluetech

@bluetech bluetech commented Mar 1, 2020

Copy link
Copy Markdown
Contributor

dict.fromkeys() has two forms:

  1. dict.fromkeys([1,2], "foo") -> {1: 'foo', 2: 'foo'}
  2. dict.fromkeys([1,2]) -> {1: None, 2: None}

Previously, the second form gave the type dict[int, None]. Presumably
this is done to accommodate the common case where the values are filled
in later.

I think type safety should trump the minor inconvenience of needing to
use the longer form, so change it to None instead.

@bluetech

bluetech commented Mar 1, 2020

Copy link
Copy Markdown
Contributor Author

Conflicts trivially with #3798 -- will rebase if needed.

@bluetech
bluetech force-pushed the dict-fromkeys-none branch from 873c2a8 to b991c36 Compare March 1, 2020 21:24
@JelleZijlstra

Copy link
Copy Markdown
Member

This breaks a test in mypy (https://github.com/python/mypy/blob/09676c94ca885929322e0d5191df04d472d4df10/test-data/unit/pythoneval.test#L210), which breaks our CI. I agree that we should make this form return dict[T, None] in agreement with the runtime behavior. The easiest way forward is probably to first change or drop the mypy testcase, then rerun CI here and merge this change.

@bluetech

bluetech commented Mar 2, 2020

Copy link
Copy Markdown
Contributor Author

Thanks @JelleZijlstra, I sent python/mypy#8474. Even if this PR is rejected due to ergonomics, I still think the mypy one should be merged, to keep the option open.

… not Any

`dict.fromkeys()` has two forms:

1. `dict.fromkeys([1,2], "foo")` -> `{1: 'foo', 2: 'foo'}`
2. `dict.fromkeys([1,2])` -> `{1: None, 2: None}`

Previously, the second form gave the type `dict[int, None]`. Presumably
this is done to accommodate the common case where the values are filled
in later.

I think type safety should trump the minor inconvenience of needing to
use the longer form, so change it to None instead.
@srittau

srittau commented Mar 2, 2020

Copy link
Copy Markdown
Collaborator

I think by using Optional[Any] we get the best of both worlds. The user is forced to handle None values, but we don't lose the ergonomy of being able to assign any value.

@bluetech
bluetech force-pushed the dict-fromkeys-none branch from b991c36 to bd658bd Compare March 2, 2020 07:33
@bluetech

bluetech commented Mar 2, 2020

Copy link
Copy Markdown
Contributor Author

@srittau I personally prefer the strict option, but Optional[Any] is also an improvement.

There is definitely an ergonomics argument to be made. Let's say we intend the value type to be Optional[int], then since dict is invariant, the longer form needs to be used, with an explicit annotation:

d: Dict[int, Optional[int]] = dict.fromkeys([1,2,3], None)

BTW, considering PEP 585 which will hopefully be implemented, I was wondering if there's actually a reason why the fromkeys stubs use the free generics _T, _S instead of the bound class generics _K, _V? Here is a test stub file which simulates using the bound variables:

from typing import Generic, TypeVar, Iterable, Optional, overload

_K = TypeVar("_K")
_V = TypeVar("_V")

class Dict(Generic[_K, _V]):
    @overload
    @classmethod
    def fromdict(cls, keys: Iterable[_K]) -> Dict[_K, None]: ...

    @overload
    @classmethod
    def fromdict(cls, keys: Iterable[_K], value: _V) -> Dict[_K, _V]: ...


d1 = Dict.fromdict([1,2,3])
reveal_type(d1) # Revealed type is 'x.Dict[builtins.int*, None]'

d2 = Dict.fromdict([1,2,3], None)
reveal_type(d2) # Revealed type is 'x.Dict[builtins.int*, None]'

d3 = Dict[int, Optional[int]].fromdict([1,2,3], None)
reveal_type(d3) # Revealed type is 'x.Dict[builtins.int*, Union[builtins.int, None]]'

Seems to work, and makes the form demonstrated in d3 possible.

@srittau

srittau commented Mar 2, 2020 •

Copy link
Copy Markdown
Collaborator

What we are really missing is a way to explicitly state the type of a type variable like other languages allow us to do:

x = dict.fromkeys[str]([1, 2, 3])  # dict[int, Optional[str]]

But that's getting off-topic now. I am fine with any solution for this PR.

@bluetech

bluetech commented Mar 7, 2020

Copy link
Copy Markdown
Contributor Author

Closed per python/mypy#8474 (comment).

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants