Q: Proper Type Hints For Generic Type Classes I am trying to write a tree node class in Python. I have a base class called Node that defines the tree semantics and subclasses implementing nodes that contain different kinds of content. I want to use type hints.
Here is a minimal implementation that creates trees of either strings or integers and enumerates them depth-first.
from typing import TypeVar, Generic, List, Iterable

T = TypeVar("T")


class Node(Generic[T]):
    def __init__(self, content: T):
        self.content = content
        self.children: List[Node[T]] = []

    def depth_first_search(self) -> Iterable["Node[T]"]:
        yield self
        for child in self.children:
            yield from child.depth_first_search()


class StringNode(Node[str]):
    def get_string(self) -> str:
        return self.content


class IntegerNode(Node[int]):
    def get_integer(self) -> int:
        return self.content


if __name__ == "__main__":
    a = StringNode("apple")
    b = StringNode("banana")
    c = StringNode("pear")
    a.children = [b, c]
    for n in a.depth_first_search():
        print(n.get_string())

    a = IntegerNode(1)
    b = IntegerNode(2)
    c = IntegerNode(3)
    a.children = [b, c]
    for n in a.depth_first_search():
        print(n.get_integer())

This code works at runtime, however, from PyCharm I get the warnings "Unresolved attribute reference 'get_string' for class 'Node'" and "Unresolved attribute reference 'get_integer' for class 'Node'" for the n.get_string() and n.get_integer() lines, respectively.
I have tried specifying various covariant and contravariant modifiers to the type variable T. In Python 3.7 I have also tried using PEP 563 by adding from __future__ import annotations and removing the quotes from the return value hint for Node.depth_first_search. None of this has had an effect.
I have tried creating "type cast" methods like the following in StringNode.
    def depth_first_search(self) -> Iterable[StringNode]:
        return super().depth_first_search()

This takes care of the warning down in the __main__ block but now I get a "Expected type 'Iterable[StringNode]', got 'Iterable[Node]' instead" warning on the return value of this method.
How do I rewrite the type hints so that I don't get warnings?

A: When a is a StringNode, a.depth_first_search() returns Iterable[Node[str]], not Iterable[StringNode].
In this case, you probably shouldn't have those get_string and get_integer methods. Just have clients access content directly, or if you're determined to have a getter for some reason, make it get_content(self) -> T in the Node base class.
I don't think Python's type annotations support having your depth_first_search return an iterable of the subclass type you want, without explicit casts or Any. You would need to be able to express the fact that self has the same type as the elements of self.children, and I don't see a way to do that.

A: There doesn't appear to be a way to use Python's type hinting to get the result I want if I make Node a base class. However, I can make it work by changing Node to a mixin.
from typing import TypeVar, Iterable, Generic

T = TypeVar("T")


class NodeMixin(Generic[T]):
    def __init__(self, *children: T):
        self.children = children

    def depth_first_search(self) -> Iterable[T]:
        yield self
        for child in self.children:
            yield from child.depth_first_search()


class StringNode(NodeMixin["StringNode"]):
    def __init__(self, content: str, *children: T):
        super().__init__(*children)
        self.content = content

    def upper(self) -> str:
        return self.content.upper()


class IntegerNode(NodeMixin["IntegerNode"]):
    def __init__(self, content: int, *children: T):
        super().__init__(*children)
        self.content = content

    def add_five(self) -> int:
        return self.content + 5


if __name__ == "__main__":
    t = StringNode("apple", StringNode("banana"), StringNode("pear"))
    for n in t.depth_first_search():
        print(n.upper())

    t = IntegerNode(1, IntegerNode(2), IntegerNode(3))
    for n in t.depth_first_search():
        print(n.add_five())

This runs correctly and PyCharm gives me no warnings. Furthermore, I do get warnings if I incorrectly try to call upper or add_five on the wrong type of object returned by NodeMixin.depth_first_search.
