Python typing for a metaclass Singleton

Question:

I have a Python (3.8) metaclass for a singleton as seen here

I’ve tried to add typings like so:

from typing import Dict, Any, TypeVar, Type

_T = TypeVar("_T", bound="Singleton")


class Singleton(type):
    _instances: Dict[Any, _T] = {}

    def __call__(cls: Type[_T], *args: Any, **kwargs: Any) -> _T:
        if cls not in cls._instances:
            cls._instances[cls] = super().__call__(*args, **kwargs)
        return cls._instances[cls]

In the line:

_instances: Dict[Any, _T] = {}

MyPy warns:

Mypy: Type variable "utils.singleton._T" is unbound

I’ve tried different iterations of this to no avail; it’s very hard for me to figure out how to type this dict.

Further, the line:

def __call__(cls: Type[_T], *args: Any, **kwargs: Any) -> _T:

Produces:

Mypy: The erased type of self "Type[golf_ml.utils.singleton.Singleton]" is not a supertype of its class "golf_ml.utils.singleton.Singleton"

How could I correctly type this?

Asked By: alexcs

||

Answers:

This should work:

from __future__ import annotations

import typing as t


_T = t.TypeVar("_T")


class Singleton(type, t.Generic[_T]):

    _instances: dict[Singleton[_T], _T] = {}

    def __call__(cls, *args: t.Any, **kwargs: t.Any) -> _T:
        if cls not in cls._instances:
            cls._instances[cls] = super().__call__(*args, **kwargs)
        return cls._instances[cls]

Rough explanations:

  1. _T = TypeVar("_T", bound="Singleton") is not correct – Singleton is type(type(obj)) where obj: _T = Singleton.__call__(...). In proper usage, the argument to bound= can only be type(obj) or some union typing construct, not type(type(obj).
  2. Type variable "_T" is unbound indicates that you need to make Singleton generic with respect to _T to bind _T.
  3. The erased type of self ... error message is telling you that you’re committing an error that will go away once you fix (1).

Runtime is important too, so the final sanity check is to make sure you’ve actually implemented a singleton using this metaclass:

class Logger(metaclass=Singleton):
    pass

>>> print(Logger() is Logger())
True
Answered By: dROOOze