Search code examples
pythonsubclasstype-hintingmypytype-variables

Dict type does not work with TypeVar using mypy?


I'm trying to create a little service locator. All services are children classes of BaseService class. I use registerService method to register a service class type with its instance and it is stored in the _services Dict. Then you can get the service instance calling getService with the class type needed. Example:

ServiceLocator.instance().registerService(LogService,LogService())
logServiceInstance =ServiceLocator.instance().getService(LogService)

This is the code showing the error:

  E = TypeVar('E', bound=BaseService)


  class ServiceLocator(QObject): #type: ignore

      _instance = None;
      allServicesInited = pyqtSignal()

      def __init__(self) -> None:
          super().__init__()
          self._instance = self

          self._services: Dict[Type[E], E] = {} # This gives error: Invalid type "servicelocator.E"

      def registerService(self, t: Type[E], instance: E)->None:
          self._services[t]=instance
      def getService(self,service: Type[E])-> E:
          return self._services[service]

I get error: Invalid type "servicelocator.E" at the line where the Dict annotation is added as I show in the code.

I'm using Python 3.6.4 and MyPy 0.590. Mypy flags are:

--ignore-missing-imports --strict

Shouldn't this work correclty?


Solution

  • No. Neither the __init__ function nor your class is generic, so the E type has no meaning within the context of the __init__ function.

    The way you'd make this typecheck is to have your ServiceLocator class be generic:

    from typing import Generic
    
    # ...snip...
    
    class ServiceLocator(QObject, Generic[E]): #type: ignore
        _instance = None;
        allServicesInited = pyqtSignal()
    
        def __init__(self) -> None:
            super().__init__()
            self._instance = self
    
            self._services: Dict[Type[E], E] = {}
    
        def registerService(self, t: Type[E], instance: E) -> None:
            self._services[t] = instance
    
        def getService(self, service: Type[E]) -> E:
            return self._services[service]
    

    That said, this would likely not do what you'd expect: you'd be insisting that your ServiceLocator is capable of storing exactly one type of service, and no other.

    What you want instead is a way of establishing some invariant on your _services field: to state that there's some relationship that must always be true of every key-value pair.

    However, AFAIK, this is impossible to do using Python's type system: dicts (and other containers) are treated in a completely homogenous way. We know the keys must be a particular type and the values must be some other type, but that's about it.

    You'll have to settle instead for checking this relationship at runtime:

    class ServiceLocator(QObject): #type: ignore
        _instance = None;
        allServicesInited = pyqtSignal()
    
        def __init__(self) -> None:
            super().__init__()
            self._instance = self
    
            self._services: Dict[Type[BaseService], BaseService] = {}
    
        def registerService(self, t: Type[E], instance: E) -> None:
            self._services[t] = instance
    
        def getService(self, service: Type[E]) -> E:
            instance = self._services[service]
            assert isinstance(instance, service)
            return instance
    

    Note the assert in the last method -- mypy is smart enough to understand basic assert and isinstance checks. So before the assert, instance is of type BaseService; after the assert mypy understands to infer that the narrowed type is E.

    (You could also use a cast, but I personally prefer using explicit runtime checks in cases where the typesystem isn't strong enough to express some constraint.)