Search code examples
pythonmypy

Mypy with class variable that is an instance of the class


I am trying to have a class with a class variable that represents an empty instance of the class. What I currently have is

from collections import namedtuple
# from typing import Optional

_Thing = namedtuple("_Thing", ["foo", "bar"])

class Thing(_Thing):
    __slots__ = ()

    def baz(self):
        print("foo", self.foo)

    # NameError: name 'Thing' is not defined
    # EMPTY = Thing(None, None)

Thing.EMPTY = Thing(None, None)

if __name__ == '__main__':
    thing = Thing.EMPTY

    thing.baz()

    print("Done")

I am also trying to run Mypy on the code. When I run python simple.py, it runs as expected:

$ python simple.py && mypy simple.py 
foo None
Done
simple.py:15: error: "Type[Thing]" has no attribute "EMPTY"
simple.py:18: error: "Type[Thing]" has no attribute "EMPTY"
Found 2 errors in 1 file (checked 1 source file)

but Mypy is unhappy because the declaration for Thing does not define EMPTY.

If I uncomment the definition of EMPTY inside the class, I get a NameError because I am trying to reference Thing while it is being defined.

If I try to declare EMPTY in the class as EMPTY = None and assign it outside the class, Mypy is unhappy because it thinks the type of EMPTY is None.

If I try to annotate EMPTY with Optional[Thing] as a type, then I get back to using Thing before it is defined.

Is there a solution to this, or do I just need to tell Mypy to ignore the EMPTY field?

I am using python 3.9.


Solution

  • You can annotate a variable without assigning it a value. This is useful here because you can't use the name of the type Thing to create an instance within its own class body, because the global name Thing doesn't get defined until after the class object is created. So you want just an annotation, with the value to be defined later.

    The lack of the global name Thing is also why your attempts so far at annotating the attribute didn't work. The solution for this is to make a forward reference using a quoted string. You can use "Thing" to annotate where an instance of your class will be, before Thing is defined.

    (Python 3.11, which comes out in fall 2022, will include PEP 673, which will offer an even better way to refer to "the current class" from within a class's body or methods: typing.Self. It would be perfect for solving our forward reference problem, but it's not out yet.)

    Annotating an attribute in a class body normally tells the type checker that the attribute will be an instance variable (this is the default assumption that type checkers will make). If you intend the attribute to be a class variable, you need to show that by using typing.ClassVar in the annotation.

    So, putting this all together, you probably want something like this:

    import typing
    
    class Thing(_Thing):
        EMPTY: typing.ClassVar["Thing"]
        #...
    
    Thing.EMPTY = Thing(None, None)
    

    If you ever upgrade this code to Python 3.11, you can replace "Thing" in the annotation with typing.Self.