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