Now that we know how to type variables and functions, typing classes is easy.

import math

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

    def distance(self, other: "Point") -> float:  # forward reference
        return math.sqrt((self.x - other.x) ** 2 + (self.y - other.y) ** 2)

p: Point = Point(0, 0)

Forward References

Notice the quoted type "Point" in the distance function:

def distance(self, other: "Point") -> float:

This is called a forward reference. Forward references allow us to use types which have not yet been defined.

Class Variables

The line between class and instance variables in Python is a bit blurred. Variables can be typed in one of three ways: as regular variables, pure class variables, or pure instance variables.

Pure class variables can only be set at the class level. Pure instance variables can only be accessed on a class instance. Regular variables can be set at the class level and overridden on an instance.

Here is a quick, contrived example using all three types of class variables:

from typing import ClassVar

class Point:
    points: ClassVar[list["Point"]] = []  # pure class
    origin: tuple[int, int] = (0, 0)  # regular

    def __init__(self, y: int, z: int):
        # instance only
        self.x = z
        self.y = y

    def add_point(cls, point: "Point"):

Point.points = []
Point.origin = (0, 0)
Point.x = 3
#> Cannot access member "x" for type "Type[Point]"
#>   Member "x" is unknown

point = Point(1, 2)
point.points = []
#> Cannot assign member "points" for type "Point"

point.x = 2
point.y = 3
point.origin = (0, 1)


The Pyright documentation details this in more depth, though it's usually enough to just know whether you need a pure class variable or a pure instance variable.