Search code examples
pythonpython-3.xclasstype-hinting

What is the point of class-level type hints in Python?


I'm trying to understand the point of class-type hints. I know that we can use type hints in the __init__ method of a class, like this:

class Foo:
    def __init__(self, x: int):
        self.x = x

However, I came across another way of defining type hints at the class level, as shown in the following example:

class Bar:
    x : int
    def __init__(self, x: int):
        self.x = x

What is the purpose of using class-level type hints like this? Do they provide any additional benefits or information compared to using type hints only in the __init__ method?


Solution

  • The "class-level type hints", despite appearing where you'd normally see class attributes, explicitly specify the expected types of instance attributes (except where ClassVar is used). As explained in PEP-526:

    Class and instance variable annotations

    Type annotations can also be used to annotate class and instance variables in class bodies and methods. In particular, the value-less notation a: int allows one to annotate instance variables that should be initialized in __init__ or __new__. The proposed syntax is as follows:

    class BasicStarship:
        captain: str = 'Picard'               # instance variable with default
        damage: int                           # instance variable without default
        stats: ClassVar[Dict[str, int]] = {}  # class variable
    

    Here ClassVar is a special class defined by the typing module that indicates to the static type checker that this variable should not be set on instances.


    Why would you use them?

    In your first example:

    class Foo:
        def __init__(self, x: int):
            self.x = x
    

    the parameter to __init__ has a type hint, preventing e.g. Foo(123), but the type of the attribute Foo("bar").x has to be inferred from the assignment self.x = x in __init__.

    By contrast, in your second example:

    class Bar:
        x : int
        def __init__(self, x: int):
            self.x = x
    

    you have an explicit definition of the type of self.x, so the assignment can actually be validated by e.g. MyPy.


    Why is that useful?

    Giving the type system more information allows it to help you more precisely. If, for example, you subsequently changed the parameter type:

    class Bar:
        x: int
        def __init__(self, x: str):
                            # ^~~
            self.x = x
    

    rather than getting errors all over your codebase where some_bar.x is accessed, assuming it's an int (as you would in the case that the attribute's type was inferred), you'd get one error pointing you straight to where it's assigned and know exactly where to fix it:

    class Bar:
        x: int
        def __init__(self, x: str):
            self.x = int(x)
    

    As always, though, remember (per the docs):

    Note The Python runtime does not enforce function and variable type annotations. They can be used by third party tools such as type checkers, IDEs, linters, etc.

    Python itself won't error on this, you need tools like MyPy to get the most use out of the annotations.