0

Let's consider an instance check for dict_keys type :

dict_keys = type({}.keys())
print(dict_keys)  # <class 'dict_keys'>
assert isinstance({}.keys(), dict_keys)

According to Python documentation .keys(), .values() and .items() all return a dictionary view which is codified in documentation as dictview view objects. However there seems to be no dictview type at least not in cPython implementation:

# same output across Python 3.7 - 3.11
print(dict_keys.mro())              # [<class 'dict_keys'>, <class 'object'>]
print({}.values().__class__.mro())  # [<class 'dict_values'>, <class 'object'>]
print({}.items().__class__.mro())   # [<class 'dict_items'>, <class 'object'>]

I have a use case where I need to accept native dictview but reject user-provided objects which quack like dictview (duck-typing is not an option by design). I can do this by separately checking for dict_keys, dict_values and dict_items, but mypy complains on:

dict_keys = type({}.keys())
# main.py:1: error: Need type annotation for "dict_keys"  [var-annotated]

A closest exposed type is types.MappingProxyType which allows for user-created dict views. It is not perfect because it has some extra methods mapping, but close enough. However, it does not work either:

from types import MappingProxyType
from typing import Type

dict_keys: Type[MappingProxyType] = type({}.keys())   # error: Incompatible types in
#   assignment (expression has type "Type[dict_keys[<nothing>, <nothing>]]",
#   variable has type "Type[MappingProxyType[Any, Any]]")  [assignment]
x: dict_keys    # error: Variable "__main__.dict_keys" is not valid as a type  [valid-type]

len(x)

The first error shows that mypy is pretty well aware of dict_keys existence, but it still insists that I should manually annotate it. The second error is not related to my core question (it is discussed in mypy "is not valid as a type" for types constructed with `type()`). mypy playground.

Is there an obvious solution that I am missing, or should I head to mypy repo and open an issue? Of course I could just use Any but this is not the kind of solution I am looking for.

krassowski
  • 13,598
  • 4
  • 60
  • 92
  • 1
    [`collections.abc.MappingView`](https://docs.python.org/3/library/collections.abc.html#collections.abc.MappingView) and [`collections.abc.KeysView`](https://docs.python.org/3/library/collections.abc.html#collections.abc.KeysView) – Mechanic Pig Dec 03 '22 at 14:28
  • `types.MappingProxyType` is completely wrong. It's barely even related to key/value/item views. – user2357112 Dec 03 '22 at 14:28
  • Yup, that's it thank you! Very obvious in retrospect. – krassowski Dec 03 '22 at 14:30
  • @krassowski: It's not. Those are ABCs, which allow non-native dict views, contrary to your stated goals. – user2357112 Dec 03 '22 at 14:31
  • For example, [`collections.UserDict().keys()`](https://ideone.com/ZMiPyV) is an instance of `collections.abc.KeysView`, but not an instance of `type({}.keys())`. – user2357112 Dec 03 '22 at 14:32
  • Yes, but I can use them for type hinting `dict_keys: Type[MappingView] = type({}.keys())` and then use `dict_keys` and friends for instance checks. You are right that I cannot use `MappingView` for instance checks. – krassowski Dec 03 '22 at 14:32
  • I am still a bit surprised that there is no runtime `dictview` though. – krassowski Dec 03 '22 at 14:34

1 Answers1

1

Mypy does know of dict_keys - it's just contained in the internal _collections_abc module. But that shouldn't be no problem. If you want to use dict_view[str, int] this problem becomes harder - but if you don't need to generic arguments it's quite simple.

from typing import TYPE_CHECKING

if TYPE_CHECKING:
   from typing import Union
   from _collections_abc import dict_keys, dict_values, dict_items
   dict_view = Union[dict_keys, dict_values, dict_items]
else:
   dict_keys, dict_values, dict_items = {}.keys(), {}.values(), {}.items()
   # if you're below 3.10 isinstance requires a tuple and doesn't work with unions
   dict_view = (dict_keys, dict_values, dict_items)

This should work so mypy complains about non-dict mapping views and that the dict_* types work for mypy and during execution.

def foo(view: dict_view):
    assert isinstance(view, dict_view)

Ping me if you need a generic dict_view as getting the correct behaviour with isinstance isn't trivial in that case.

Robin Gugel
  • 853
  • 3
  • 8