0

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] 
5
  • 2
    is_line and is_circle are both redundant. You can replace, e.g, item.is_line with isinstance(item, Line). Commented Feb 11, 2022 at 21:10
  • 1
    is_horizontal and radius should both be instance attributes, not class attributes. Not all lines are horizontal, and not all circles have radius 10. Commented Feb 11, 2022 at 21:11
  • 1
    You specify items -> List[Geometry] so item in items: item.is_horizontal seems a valid warning. You mean item.is_line and item.is_horizontal should short-circuit the is_horizontal check? Commented Feb 11, 2022 at 21:13
  • A static typechecker cannot assume that item.is_horizontal will only be evaluated when item has a runtime type of Line. You said it was a Geometry, so it has to assume it might not be a Line. You can use cast to tell the type checker it's OK: if item.is_line and cast(Line, item).is_horizontal. Commented Feb 11, 2022 at 21:20
  • All the properties shown here are obviously wrong. This is a stub of the class, enough to show what I need to show for type hinting. Commented Feb 11, 2022 at 21:21

2 Answers 2

1

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.

Sign up to request clarification or add additional context in comments.

5 Comments

You are telling me to change the code, not the type hinting. I have good reason in my real code not to use isinstance. I would like to know if there is a generic way to do the same as Union[A,B,C,...] where B, C and ... are subclasses of A
@stenci: Yup. If you want to use static type checking, you need to write code that is statically type-safe - that or throw Any and cast and #type:ignore all over the place, but then you'd only be pretending to type check your code.
Your comment is the answer to my question: you can't. Well... restricting the type to all subclasses of a given class is not pretending to type check. It is not perfect, but there is a lot of value in it. I want PyCharm to tell me "you can use Geometry or any subclass of it". It is not perfect, but it's better than Any or no type check at all.
Also, that union isn't type-safe either. The warning going away when you use the union might be a bug in your IDE's type checker. mypy correctly reports the error as long as you annotate the method - by default, mypy doesn't check unannotated functions.
I am not trying to make type safe code. I'm trying to get the most out of type warnings, autocompletion and documentation, which in this case it means making sure that I'm using subclasses of a specific class.
1

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

3 Comments

The cast isn't necessary after the isinstance check - mypy automatically uses the narrowed type.
Although since the OP seems to be using something other than mypy - mypy would have given different output - maybe they do need the cast.
Yeah, I guess I'm taking the view the cast is necessary in general, though any particular type checker might have hard-coded optimizations like recognizing what it would mean for isinstance to return True in this context.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.