typeshed
typeshed copied to clipboard
Improve documentation for decisions around certain `list`, `dict`, `Mapping`, and `Sequence` methods
list.index and list.count currently only accepts values of the element type and Sequence.index and Sequence.count use Any for the type of the argument. In my opinion all of these functions could safely take object.
dict.get and Mapping.get currently only accepts values of the key type. This could be safely extended to be any Hashable object. Non-Hashable values will raise TypeError, but runtime safely accepts values of any other type. I ran into someone on the gitter who needed this a few weeks ago, but he never opened an issue.
I would be willing to make a PR for this.
It's true that these functions will take any argument at runtime and return a result, but I'm not convinced that that's what we should be doing in the stub. list[int].index("str") may work, but it's almost certainly not what the user wants, so I'd like type checkers to be able to tell that this code is suspicious.
These kinds of methods are typeshed top hits. E.g., just last week: https://github.com/python/typeshed/pull/6922 I think it'd be helpful to document these decisions, at least as comments in the stubs.
This diverges what is valid according to typing and what is valid at runtime in core libraries, which makes annotating existing code bases that might utilize this behavior correctly to be considered incorrect. I didn't know it was in typing's scope to decide that certain core Python behavior isn't valid, are you sure that isn't overreaching?
typeshed's purpose is to help developers to find mistakes in their code. As @JelleZijlstra pointed out, these methods are typed to find likely problems, not necessarily to accept everything that doesn't crash at runtime.
Ideally we would have a way to say "this method accepts any type that overlaps with this type", so you could do list_of_strings.index(string_or_none) or list_of_strings_or_nones.index(string), but not list_of_ints.index(string).
All of the examples I could come up with (see one below) would be serviced by a Super, denoting that any supertype is accepted, not subtype. Something that checks for Union overlapping would be even less strict, but would have very limited use cases otherwise. Something like Super could be used in many cases.
This is just an stupid example of a pattern I have used and seen others use before. You handle some cases using a dict and .get() and if .get() returns the sentinel you continue on and do something else. In these cases the type of the argument is usually a supertype/superset of the type of the dict.
def stringify(value: object) -> str:
# handle a few of the values first
a = ({None: "None", True: "True", False: "False", ...: "..."}).get(value)
if a is not None:
return a
# rest are handled by arbitrary logic
...