1

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" ?

STerliakov
  • 4,983
  • 3
  • 15
  • 37
Danielle M.
  • 3,607
  • 1
  • 14
  • 31
  • A similar question (mine): [PyCharm gives me a type warning about my metaclass; mypy disagrees](https://stackoverflow.com/q/76224359). – InSync Jul 14 '23 at 22:38

3 Answers3

1

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
juanpa.arrivillaga
  • 88,713
  • 10
  • 131
  • 172
  • ty, particularly for addressing whether it's pythonic for A.__new__ to return something other than A (or a subclass thereof). Given that GVR himself says no, I'll re-implement using one of your methods. Ty again! – Danielle M. Jul 15 '23 at 19:07
1

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.

wallace
  • 11
  • 1
  • 2
  • 2
    Have you actually checked if using that library/code would solve the typing issues? I suspect since it is untyped, it would just lead to more of a headache. And note, it would replace your class that you decorated with *another class*, and that will wreak all sorts of havoc for the static analysis – juanpa.arrivillaga Jul 14 '23 at 21:13
1

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
Plagon
  • 2,689
  • 1
  • 11
  • 23