pythonmypytyping

mypy & typing singleton / factory classes


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


Solution

  • 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