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