-
-
Notifications
You must be signed in to change notification settings - Fork 1.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Overload signature of get
to return an Optional value and to allow default to take any type to match runtime behavior.
#822
Conversation
7f4440a
to
ccdd564
Compare
…default to take any type to match runtime behavior. This chage more closely matches the behavior of `get` at runtime. Users can pass whatever they want in to the default parameter and it will be returned if the key is absent. Additionally, `get` should return an `Optional` if called with only one parameter. ```python z = {'a': 22} reveal_type(z.get('b')) reveal_type(z.get('b', 22)) reveal_type(z.get('b', 'hello')) ``` Before: ```shell test_get_default.py:2: error: Revealed type is 'builtins.int*' test_get_default.py:3: error: Revealed type is 'builtins.int*' test_get_default.py:4: error: Revealed type is 'builtins.int*' test_get_default.py:4: error: Argument 2 to "get" of "dict" has incompatible type "str"; expected "int" ``` After: ```shell test_get_default.py:2: error: Revealed type is 'Union[builtins.int*, builtins.None]' test_get_default.py:3: error: Revealed type is 'builtins.int' test_get_default.py:4: error: Revealed type is 'Union[builtins.int, builtins.str*]' ```
ccdd564
to
1917638
Compare
LGTM. We also tested this against mypy's own tests with master, everything passes. Rationale for accepting:
|
A heads up that this actually caused a bunch of errors in one of our internal codebase. I don't have time to go over those right now to see whether they are newly discovered bugs in our code or point to cases where the new signature of get() is still problematic, but I'll get back to this later. |
Will you have time to go over this before 0.470 or would you prefer this be temporarily reverted? |
I propose to just not sync typeshed past this commit. For 0.470 we may not sync typeshed at all, or only past the commit that restores the symlink. |
@gvanrossum yeah, this could be a fairly disruptive chance to pull in last minute into FWIW we found a bunch of legit bugs in our code base when I added this stub in. Things of the form: for x in foo.get('hello'): # oops
pass |
This issue comes back and back again, see python/mypy#278 for links to several PRs and issues about it. What this particular PR changes is there is now one new Union[] as a return type. I recall @JukkaL mentioning a few times that currently Mypy has issues dealing with it (for example, see relevant comment here). If this is what @gvanrossum is seeing failing now in the Dropbox codebase, a workaround in this case would be to rework the union into two separate overloads. Before: @overload
def get(self, k: _KT) -> Optional[_VT]: ...
@overload
def get(self, k: _KT, default: _T) -> Union[_VT, _T]: ... After: @overload
def get(self, k: _KT) -> Optional[_VT]: ...
@overload
def get(self, k: _KT, default: _T) -> _VT: ...
@overload
def get(self, k: _KT, default: _T) -> _T: ... |
Yeah, I think this is indeed what is happening. And long story short (explained in detail by Jukka in python/mypy#1693), Mypy considers operations to be valid for unions when they are valid for each element of the union. For return types, this surprisingly limits and not broadens what the type checker considers valid. If you want to broaden, you should use overloads with different argument signatures instead. We should definitely get #1693 out there in docs, and possibly amend PEP 484 with a section about this. It's a subtlety that is bound to bite us in the future. |
Please do send a PR for PEP 484!
|
I will. In this particular case, I think the Union return type is a reasonable thing to do, though. It warns the user that in fact the operation he/she considers safe, might not be. Consider this example: d = {1: 2, 3: 4, 5: 6} # type: Dict[int, int]
if len(d) < d.get(1) + 2:
print('len(d)')
if len(d) < d.get(2, None) + 2:
print('len(d)')
if len(d) < d.get(3, 'str') + 2:
print('len(d)') Before #822 landed, Mypy (with
This is incorrect, get() actually accepts anything. After #822 landed, Mypy correctly points out all problems in all three cases:
So, my conclusion is that in this case we really should keep the Union as a return type. Same with |
The three-overload version is problematic as well, since mypy doesn't like when multiple overload variants match (that is another mypy issue...). The union return type seems fine to me -- I'd like see examples where it causes problems with Dropbox code. Maybe it's just something we need to fix in mypy, or a problem in the code. I'd argue that mypy treats union types correctly. For example, The typical problems with union return types should not apply to
Now |
This is specifically against python/typeshed@4603baa as requested by @gvanrossum in python/typeshed#822 for the upcoming 0.470 release. Tests pass.
Agreed, this only needs documentation, as per python/mypy#1693! The one possible improvement to mypy itself that I recall is making mypy not report errors when the user is performing an operation that happens to be valid for all elements in the union. I can't find the specific PR/issue where this was raised though, so correct me if I'm wrong here. |
Usually this works, e.g. this is fine: class A:
def f(self): pass
class B:
def f(self): pass
def f(x: Union[A, B]):
x.f() but there are many places in mypy where Union has to be special-cased, and I believe some of these have not yet been discovered. We've definitely fixed a smattering of these over the past year. |
This chage more closely matches the behavior of
get
at runtime. Users can pass whatever they want in to the defaultparameter and it will be returned if the key is absent. Additionally,
get
should return anOptional
if called withonly one parameter.
test_get_default.py
Before:
After: