16
votes

I want to add type hints to a function that will accept any object with a __getitem__ method. For instance, in

def my_function(hasitems, locator):
    hasitems[locator]

I don't want to restrict hasitems to be a specific type like list or dict. As long as it supports __getitem__, it's an appropriate argument to my_function. How can I annotate its type without being unnecessarily restrictive?

Edit: apparently PyCharm can deduce the appropriate hint in a number of common cases, but not in my actual use case. I can't post the code since it's for work, and I haven't been able to find a nonproprietary minimal example where PyCharm fails. In any case, the original question doesn't reference PyCharm and it is still a valid use case for type hints.

3
Make an abc with that single abstract method. - juanpa.arrivillaga
May I ask what is the use for such a hint? IDEs already deduct the hints automatically based on the usage. Are you using these hints for something else? - Liran Funaro
Then upgrade. For me PyCharm yields for calling my_function(5, 10): > Type 'int' doesn't have expected attribute 'getitem' less... (Ctrl+F1) Inspection info: This inspection detects type errors in function call expressions. Due to dynamic dispatch and duck typing, this is possible in a limited but useful number of cases. Types of function parameters can be specified in docstrings or in Python 3 function annotations. - Liran Funaro
I suggest you put your actual code in the question. This way we could be more helpful. - Liran Funaro
What Python version are you using? 3.7 introduced Protocols, which are supposed to address this problem: python.org/dev/peps/pep-0544 - Patrick Haugh

3 Answers

9
votes

If you're willing to install a not-quite-offical extension to typing, typing-extensions, you can use a Protocol, which should be an implementation of PEP-0544:

from typing_extensions import Protocol
from typing import Any

class GetItem(Protocol):
    def __getitem__(self: 'Getitem', key: Any) -> Any: pass

class BadGetItem:
    def __getitem__(self, a: int, b: int) -> Any: pass

def do_thing(arg: GetItem):
    pass

do_thing(dict())  # OK
do_thing(BadGetItem())  # Fails with explanation of correct signature
do_thing(1)  # Fails
1
votes

It sounds like you essentially want to define your own abstract base class (abc).

Following the documentation above, you can define a custom abc that only dictates the presence of __getitem__, but let's use a predefined one for an example. The Mapping abc consists of __getitem__ and a few other magic methods. You can use abcs in isinstance, but you can also directly use them as a type annotations:

def foo(bar: Mapping):
    pass

Or, using the extended type hinting support of ABCs do even fancies things, which you already saw in other answers:

def foo(bar: Mapping[Any, Any]):
    pass
1
votes

This will work for dict and list, but not for any generic type:

from typing import Any, Mapping, Sequence, Union

def my_function(hasitems: Union[Mapping, Sequence], locator: Any) -> Any:
    return hasitems[locator]