mypy & typing singleton / factory classes

Question:

I often use the following construct to generate singletons in my code:

class Thing:
    pass


class ThingSingletonFactory:
    _thing = None

    def __new__(cls) -> Thing:
        if cls._thing is None:
            cls._thing = Thing()
        return cls._thing


def get_thing() -> Thing:
    return ThingSingletonFactory()


thing = get_thing()
same_thing = get_thing()

assert thing is same_thing

class ThingSingletonFactory stores the only instance of Thing, and returns it anytime a new ThingSingletonFactory() is requested. Works great for API clients, logging.Logger, etc.

I’m adding mypy type checking to an existing project that uses this, and mypy does not like it, at all.

line 8:  error: Incompatible return type for "__new__" (returns "Thing", but must return a subtype of "ThingSingletonFactory")  [misc]
line 15: error: Incompatible return value type (got "ThingSingletonFactory", expected "Thing")  [return-value]

I feel the type hints in the code are correct: __new__() does return the type Thing, as does the func get_thing().

How can I provide mypy the hints required to make it happy? Or is this construct simply considered "bad" ?

Asked By: Danielle M.

||

Answers:

So, this error message implies to me that mypy just doesn’t want to accept an A.__new__ that doesn’t return a subtype of A. This is probably reasonable, although fo course, in Python, you don’t have to do that.

I found this interesting discussion in a mypy issue where none-other than Guido van Rossum himself states that he doesn’t think this should ever happen.

Let me suggest a couple of alternatives 1) ditch the factory class:

import typing

class Thing:
    pass

_thing: Thing | None  = None
def thing_factory() -> Thing:
    global _thing
    if _thing is None:
        _thing = Thing()
    return _thing


thing = thing_factory()
same_thing = thing_factory()

assert thing is same_thing

I actually think the above is more pythonic anyway, the intermediate ThingFactory class serves no purpose. But the mutable global state bothers you, you can do something like:

import typing

class Thing:
    pass

class ThingFactory:
    _thing: typing.ClassVar[Thing]
    @classmethod
    def get_thing(cls) -> Thing:
        if not hasattr(cls, "thing"):
            cls._thing = Thing()
        return cls._thing

get_thing = ThingFactory.get_thing

thing = get_thing()
same_thing = get_thing()

assert thing is same_thing

Again, the intermediate class bothers me. And you do need to use ThingFactory.get_thing() instead of ThingFactory(), but it looks lie in practice you just use a function, get_thing anyway. I think that may be an adequate trade-off if you just want to placate mypy.

Finally, I should point out, that your original code raises no errors with pyright:

jarrivillaga-mbp16-2019:~jarrivillaga$ pyright test.py
No configuration file found.
...
Assuming Python platform Darwin
Searching for source files
Found 1 source file
0 errors, 0 warnings, 0 infos

Completed in 0.545sec
Answered By: juanpa.arrivillaga

Type checking of mypy (or any other Python type checker, for that matter) does not happen at runtime.

And it insists, quite reasonably, that a return value of the __new__() method of a class should be an instance of the class itself.

So in the first error, mypy complains about the return type of __new__(). Separately, in the second error, it complains that the return type of get_thing() doesn’t match the type hint.

You can create a singleton using the singleton-decorator library. Or if you don’t want non-standard library dependencies, you can check out its source code – it’s pretty short and concise.

Answered By: wallace

Another alternative is the use the built-in functools.cache:

from functools import cache

class Thing:
    pass


@cache
def get_thing() -> Thing:
    return Thing()


get_thing() is get_thing() # True
Answered By: Plagon
Categories: questions Tags: , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.