1

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?

alexcs
  • 480
  • 5
  • 16
  • don't do metaclass to get a singleton. Just create a class, and create a instance for it at a module top-level and forget about the class: there is your singleton – jsbueno Feb 01 '23 at 13:52
  • 1
    I know full-well that modules are only imported once and how to replicate a singleton that way. It does not fit my use case - I need to control the initialization timing explicitly, and I know what I'm doing. This is not a discussion on the singleton pattern, it's a discussion of how to type a metaclass that happens to be a singleton. I've added the singleton reference in the title because it's a continuation of a highly visited post on the topic and others may find it useful. – alexcs Feb 01 '23 at 16:00
  • 1
    ok - thanks for your reply. Mine was more or less a reminder I leave on all metaclass-singleton questions (when not answering them) - so to dissuade this culture when uneeded. Obviously there are cases when it is the way to go. – jsbueno Feb 01 '23 at 16:13

2 Answers2

4

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've "erased" the type checker's inferred type* of cls. Technically speaking, __call__ is the same on a metaclass as any other instance method - the first argument is simply the type of the owning class. In the current static typing system, however, a metaclass's instance method's first argument is not in concordance with type[...].

*The inferred type is explicitly Self in the following:

import typing as t

Self = t.TypeVar("Self", bound="A")

class A:
    def instancemethod(arg: Self) -> None:
        pass
    @classmethod
    def classmethod_(arg: type[Self]) -> None:
        pass

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
dROOOze
  • 1,727
  • 1
  • 9
  • 17
  • 1
    This works, thanks a lot. However with the rest of your changes, it works whether "bound = Singleton" is there or not (at least mypy doesn't complain). I think I need to review the fundamentals of some of these primitives (Typevar, metaclasses and Generic) to fully comprehend this. There's plenty in your answer to get me started, thanks. – alexcs Feb 01 '23 at 09:42
  • 2
    @alexcs Type checkers' code analysis on metaclasses are very primitive (but they're a work in progress, so expect improvements). However, there's a non-metaclass-related issue that `mypy` should pick up if you run `mypy` in `--strict` mode. Try putting `bound="Singleton"` again under `--strict`, and you should get a complaint about a missing type variable. – dROOOze Feb 01 '23 at 09:46
  • 1
    @alexcs I've left (3) way too vague; hopefully the edits help a bit more. – dROOOze Feb 01 '23 at 11:07
  • I am getting `name 'Singleton' is not defined` for the field `_instances` type annotation. This `"Singleton[_T]"` seems to work though. – Wereii Mar 15 '23 at 13:31
  • @Wereii Did you put `from __future__ import annotations` at the top of the module, like in the code snippet in the answer? – dROOOze Mar 16 '23 at 09:30
0

With the solution above, when the Singleton class is used as a metaclass the following issue in pylance:

Expected type arguments for generic class "Singleton" PylancereportMissingTypeArgument.

Looks like you have to use: class Logger(metaclass=Singleton["Logger"]): pass