Home > Enterprise >  Type hinting on Python subclasses
Type hinting on Python subclasses

Time:02-12

The List[Geometry] type hint on the code below does not cause self.items.append(line) to complain because line is a Line which is subclass of Geometry.

The same type hint causes the item.is_horizontal on the last line to say Unresolved attribute reference 'is_horizontal' for class 'Geometry'.

I think this happens because the is_horizontal property exists only in one of the subclasses of Geometry.

What is the correct way to avoid this warning?

With List[Union[Geometry, Line, Circle]] the warning goes away, but I'm not sure this is the correct solution. Listing a set of classes is not like saying <Geometry> and all subclasses.

class Geometry:
    is_circle = is_line = False

class Line(Geometry):
    is_line = True          # property on Geometry and subclasses
    is_horizontal = True    # property only on Line

class Circle(Geometry):
    is_circle = True        # property on Geometry and subclasses
    radius = 10             # property only on Line

class Geometries:
    def __init__(self):
        self.items: List[Geometry] = []

    def add_line(self, line: Line):
        self.items.append(line)

    def horizontal_lines(self):
        return [item for item in self.items if item.is_line and item.is_horizontal]

CodePudding user response:

Use an actual type check, with isinstance:

def horizontal_lines(self):
    return [item for item in self.items if isinstance(item, Line) and item.is_horizontal]

From a static typing perspective, there is no connection between an is_line attribute and whether or not an object is an instance of Line. There could be other subclasses of Geometry somewhere else in the program with is_line = True but no is_horizontal, or instances could have is_line set at the instance level.

CodePudding user response:

Since items has a static type of List[Geometry], the type checker cannot make any assumptions about what attributes a list element has beyond what is statically defined in Geometry. Since is_horizontal is not an attribute of all instances of Geometry, an access like item.is_horizontal is forbidden.

However, you can assert that item is really a Line based on runtime knowledge using cast. cast(Line, item) is a no-op at runtime, but the type checker treats it as meaning "whatever type item is, cast will return an instance of Line.

Given some more realistic definitions of Geometry, Line, and Circle,

from typing import cast, List


class Geometry:
    ...


class Line(Geometry):
    def __init__(self, slope, y_intercept):
        ...

    @property
    def is_horizontal(self):
        return self.slope == 0

    ...


class Circle(Geometry):
    def __init__(self, radius, center):
        self.radius = radius
        self.center = center

    ...

you can define Geometries (and in particular, Geometries.horizontal_lines) like so:

class Geometries:
    def __init__(self):
        self.items: List[Geometry] = []

    def add_line(self, line: Line):
        self.items.append(line)

    def horizontal_lines(self) -> List[Line]:
        return [item for item in self.items
                     if isinstance(item, Line) 
                        and cast(Line, item).is_horizontal]

Various static type-checkers may make the call to cast optional. For example, they could infer that given isinstance(x, y) and z, if isinstance returns True then the static type of x can be assumed to be y in z, regardless of any previous type hints regarding the static type of x. (If it were to return False, then z is dead code and needn't be checked at all.)

  • Related