typeshed icon indicating copy to clipboard operation
typeshed copied to clipboard

dict: on retrieval, key allowed to be null

Open refack opened this issue 3 months ago • 4 comments

image

> uv run --with pyright pyright  .\dict-none-key.py 
e:\1work\CFF\dict-none-key.py
  e:\1work\CFF\dict-none-key.py:4:12 - error: Argument of type "int | None" cannot be assigned to parameter "key" of type "int" in function "get"
    Type "int | None" is not assignable to type "int"
      "None" is not assignable to "int" (reportArgumentType)

But

> python -c "a = {'A': 1};b = {1: 'א'};k1 = a.get('A');k2 = b.get(k1);print('no runtime error')"                                                                                                                                   
no runtime error      

refack avatar Sep 17 '25 23:09 refack

It just came to me that this might have been a design decision as:

a = {'A': 1}
b: dict[int | None, str] = {1: 'א'}
k1 = a.get('A')
k2 = b.get(k1)

gives:

> uv run --with pyright pyright  .\dict-none-key.py                                             
0 errors, 0 warnings, 0 informations

I searched manually, and with copilot and found no direct mention...

Is this PR acceptable in principal, which in that case I'll cross all the I's and dot all the T's

refack avatar Sep 17 '25 23:09 refack

It's sort of questionable to me to treat None specially here. I think we've had someone before trying this (specifically for dict.get) and we rejected it, but can't immediately find it back. dict.get has seen a lot of efforts to change things: https://github.com/python/typeshed/pulls?q=is%3Apr+is%3Aclosed+dict.get+

JelleZijlstra avatar Sep 18 '25 01:09 JelleZijlstra

dict.get has seen a lot of efforts to change things:

I assumed so, but still somewhere there is an incongruency since:

a = {'A': 1}
b = {1: 'א'}

k1 = 'B'
kv1 = a.get(k1)
v1 = b.get(kv1)
print(f"'{k1}' -> '{kv1}' -> '{v1}'")

k2 = 'A'
kv2 = a.get(k2)
v2 = b.get(kv2)
print(f"'{k2}' -> '{kv2}' -> '{v2}'")

gets:

> python .\dict-none-key.py
'B' -> 'None' -> 'None'
'A' -> '1' -> 'א'

and IMHO is reasonable use case, where .get is used specifically for fall trough cascade

yet pyright and mypy report "error"

> uv run --with pyright pyright  .\dict-none-key.py 
dict-none-key.py
  dict-none-key.py:6:12 - error: Argument of type "int | None" cannot be assigned to parameter "key" of type "int" in function "get"
    Type "int | None" is not assignable to type "int"
      "None" is not assignable to "int" (reportArgumentType)
  dict-none-key.py:11:12 - error: Argument of type "int | None" cannot be assigned to parameter "key" of type "int" in function "get"
    Type "int | None" is not assignable to type "int"
      "None" is not assignable to "int" (reportArgumentType)
2 errors, 0 warnings, 0 informations

> uv run mypy  .\dict-none-key.py                   
dict-none-key.py:6: error: Argument 1 to "get" of "dict" has incompatible type "int | None"; expected "int"  [arg-type]
dict-none-key.py:11: error: Argument 1 to "get" of "dict" has incompatible type "int | None"; expected "int"  [arg-type]
Found 2 errors in 1 file (checked 1 source file)

You could argue it's a mypy/pyright/jetbrains bug where they are being presumptuous in inferring the type of b as dict[int,str] image

refack avatar Oct 10 '25 20:10 refack

It's sort of questionable to me to treat None specially here. I think we've had someone before trying this (specifically for dict.get)

IMHO only .get and .pop should be treated this way.

refack avatar Oct 10 '25 20:10 refack