Why does typing break when I convert my classes to generic classes?
22:03 27 Aug 2026

In the first example below are some classes representing nodes and node items in a node tree with a corresponding string path. I wanted to convert these classes to generics so I can use them for paths with different path separators. The following example is the classes converted to generic classes. I got most of it working (functionally it's good) but now in some sections there are typing errors showing. How can I fix these?

### without generic classes
import logging

logger = logging.getLogger(__name__)

class ItemRegistry[K, I](dict[K, I]):
    """
    generic base class implementing a registry for accessing and storing items with a unique key\n
    provides protection against overwriting existing items
    """

    ITEM_TYPE : str = 'item'
    item_count: int

    def __init__(self):
        logger.debug(f"creating {self}")
        super().__init__()
        self.item_count = 0

    def __str__(self):
        return f"{self.__class__.__name__}"

    def add_item(self, key: K, item: I, overwrite: bool = False) -> None:
        """
        adds an item to the registry\n
        overwrites the existing item when overwrite is True\n
        raises KeyError if an item with given key already exists and overwrite is False
        """
        logger.debug(f"{self.ITEM_TYPE} added - key: {key}, item: {item}")
        try:
            self[key]
        except KeyError:
            self[key] = item
            self.item_count += 1
            return
        else:
            if overwrite:
                self[key] = item
                self.item_count += 1
                logger.warning(f"{self.ITEM_TYPE} overwritten - key: {key}, item: {item}")
                return
            else:
                msg = f"{self.ITEM_TYPE} already exists - key: {key}, item: {item}"
                logger.error(msg)
                raise KeyError(msg)

    def remove_item(self, key: K):
        """removes item from the registry - raises KeyError if an item does not exist for the given key"""
        try:
            del self[key]
        except KeyError:
            msg = f"{self.ITEM_TYPE} does not exist with key: {key}"
            logger.error(msg)
            raise KeyError(msg)
        else:
            self.item_count -= 1
            logger.debug(f"{self.ITEM_TYPE} removed - key: {key}")

class NodeRegistry(ItemRegistry[str, 'Node | NodeItem']):
    ITEM_TYPE : str = 'node'

class NodePath(str):
    """class representing a node path"""

    SEPARATOR = '.'

    @property
    def parts(self):
        return self.get_parts(self)

    def __add__(self, value: str) -> NodePath:
        return NodePath(f"{self}{self.SEPARATOR}{value}")

    @classmethod
    def get_parts(cls, path: str) -> list[str]:
        return path.split(cls.SEPARATOR)

class NodeItem:
    """class representing a node item (a node with no children)"""

    parent: Node | NodeTree
    path: NodePath

    def __init__(self, name: str, parent: Node | NodeTree) -> None:
        self.parent = parent
        self.path = parent.path + name
        logger.debug(f"creating {self}")
        parent.add_node(self)

    def __str__(self) -> str:
        return f"{self.__class__.__name__}({self.path})"

    __repr__ = __str__

    @property
    def name(self):
        return self.path.parts[-1]
    
    def get_root_node(self) -> Node | NodeItem:
        node = self
        while not isinstance(node.parent, NodeTree):
            node = node.parent
        return node

    def get_tree(self) -> NodeTree:
        parent = self.parent
        while not isinstance(parent, NodeTree):
            parent = parent.parent
        return parent

class Node(NodeItem, NodeRegistry):
    """class representing a node"""

    parent: Node | NodeTree
    path: NodePath

    def __init__(self, name: str, parent: Node | NodeTree) -> None:
        NodeItem.__init__(self, name, parent)
        NodeRegistry.__init__(self)

    def add_node(self, node: Node | NodeItem) -> None:
        self.add_item(node.name, node)

class NodeTree(NodeRegistry):
    """class representing a node tree"""

    path: NodePath

    def __init__(self, path: NodePath) -> None:
        self.path = path
        super().__init__()

    def __str__(self):
        return f"{self.__class__.__name__}({self.path})"

    __repr__ = __str__

    def add_node(self, node: Node | NodeItem) -> None:
        self.add_item(node.name, node)

    def get_node(self, path: NodePath) -> Node | NodeItem:
        node = self[path.parts[1]]
        for part in path.parts[2:]:
            if isinstance(node, Node):
                node = node[part]
        return node
### with generic classes
import logging
from typing import Self
logger = logging.getLogger(__name__)

class ItemRegistry[K, I](dict[K, I]):
    """
    generic base class implementing a registry for accessing and storing items with a unique key\n
    provides protection against overwriting existing items
    """

    ITEM_TYPE : str = 'item'
    item_count: int

    def __init__(self):
        logger.debug(f"creating {self}")
        super().__init__()
        self.item_count = 0

    def __str__(self):
        return f"{self.__class__.__name__}"

    def add_item(self, key: K, item: I, overwrite: bool = False) -> None:
        """
        adds an item to the registry\n
        overwrites the existing item when overwrite is True\n
        raises KeyError if an item with given key already exists and overwrite is False
        """
        logger.debug(f"{self.ITEM_TYPE} added - key: {key}, item: {item}")
        try:
            self[key]
        except KeyError:
            self[key] = item
            self.item_count += 1
            return
        else:
            if overwrite:
                self[key] = item
                self.item_count += 1
                logger.warning(f"{self.ITEM_TYPE} overwritten - key: {key}, item: {item}")
                return
            else:
                msg = f"{self.ITEM_TYPE} already exists - key: {key}, item: {item}"
                logger.error(msg)
                raise KeyError(msg)

    def remove_item(self, key: K):
        """removes item from the registry - raises KeyError if an item does not exist for the given key"""
        try:
            del self[key]
        except KeyError:
            msg = f"{self.ITEM_TYPE} does not exist with key: {key}"
            logger.error(msg)
            raise KeyError(msg)
        else:
            self.item_count -= 1
            logger.debug(f"{self.ITEM_TYPE} removed - key: {key}")

class NodeRegistry[N: Node, I: NodeItem](ItemRegistry[str, N | I]):
    ITEM_TYPE : str = 'node'

class NodePath(str):
    """class representing a node path"""

    SEPARATOR = '.'

    @property
    def parts(self):
        return self.get_parts(self)

    def __add__(self, value: str) -> Self:
        return self.__class__(f"{self}{self.SEPARATOR}{value}")

    @classmethod
    def get_parts(cls, path: str) -> list[str]:
        return path.split(cls.SEPARATOR)

class NodeItem[P: NodePath, I: NodeItem, N: Node, T: NodeTree]:
    """class representing a node item (a node with no children)"""

    parent: N | T
    path: P

    def __init__(self, name: str, parent: N | T) -> None:
        self.parent = parent
        self.path = parent.path + name
        logger.debug(f"creating {self}")
        parent.add_node(self)

    def __str__(self) -> str:
        return f"{self.__class__.__name__}({self.path})"

    __repr__ = __str__

    @property
    def name(self):
        return self.path.parts[-1]
    
    def get_root_node(self) -> N | I:
        node = self
        while not isinstance(node.parent, NodeTree):
            node = node.parent
        return node

    def get_tree(self) -> T:
        parent = self.parent
        while not isinstance(parent, NodeTree):
            parent = parent.parent
        return parent

class Node[P: NodePath, I: NodeItem, N: Node, T: NodeTree](NodeItem[P, I, N, T], NodeRegistry[N, I]):
    """class representing a node"""

    parent: N | T
    path: P

    def __init__(self, name: str, parent: N | T) -> None:
        NodeItem.__init__(self, name, parent)
        NodeRegistry.__init__(self)

    def add_node(self, node: N | I) -> None:
        self.add_item(node.name, node)

class NodeTree[P: NodePath, I: NodeItem, N: Node](NodeRegistry[N, I]):
    """class representing a node tree - a hierarchial structure of nodes and node items"""

    path: P

    def __init__(self, path: P) -> None:
        self.path = path
        super().__init__()

    def __str__(self):
        return f"{self.__class__.__name__}({self.path})"

    __repr__ = __str__

    def add_node(self, node: N | I) -> None:
        self.add_item(node.name, node)

    def get_node(self, path: P) -> N | I:
        node = self[path.parts[1]]
        for part in path.parts[2:]:
            if isinstance(node, Node):
                node = node[part]
        return node

class SlashPath(NodePath):
    SEPARATOR: str = '/'

class SlashNodeItem(NodeItem[SlashPath, 'SlashNodeItem', 'SlashNode', 'SlashTree']): ...

class SlashNode(Node[SlashPath, SlashNodeItem, 'SlashNode', 'SlashTree']): ...

class SlashTree(NodeTree[SlashPath, SlashNodeItem, SlashNode]): ...

path1 = SlashPath('root')
tree = SlashTree(path1)
a = SlashNode('a', tree)
b = SlashNodeItem('b', a)
a_root = a.get_root_node()
b_tree = b.get_tree()

The errors:

        self.path = parent.path + name # line 85
Cannot assign to attribute "path" for class "NodeItem[P@NodeItem, I@NodeItem, N@NodeItem, T@NodeItem]*"
  Type "P@Node | Unknown" is not assignable to type "P@NodeItem"
        return node # line 102
Type "Self@NodeItem[P@NodeItem, I@NodeItem, N@NodeItem, T@NodeItem] | Node[P@Node, I@Node, N@Node, T@Node]*" is not assignable to return type "N@NodeItem | I@NodeItem"
  Type "Self@NodeItem[P@NodeItem, I@NodeItem, N@NodeItem, T@NodeItem] | Node[P@Node, I@Node, N@Node, T@Node]*" is not assignable to type "N@NodeItem | I@NodeItem"
    Type "Self@NodeItem[P@NodeItem, I@NodeItem, N@NodeItem, T@NodeItem] | Node[P@Node, I@Node, N@Node, T@Node]*" is not assignable to type "I@NodeItem"
    Type "Self@NodeItem[P@NodeItem, I@NodeItem, N@NodeItem, T@NodeItem]" is not assignable to type "N@NodeItem | I@NodeItem"
      Type "Self@NodeItem[P@NodeItem, I@NodeItem, N@NodeItem, T@NodeItem]" is not assignable to type "I@NodeItem"
      Type "Self@NodeItem[P@NodeItem, I@NodeItem, N@NodeItem, T@NodeItem]" is not assignable to type "N@NodeItem"
        return parent # line 108
Type "NodeTree[Unknown, Unknown, Unknown]*" is not assignable to return type "T@NodeItem"
  Type "NodeTree[Unknown, Unknown, Unknown]*" is not assignable to type "T@NodeItem"
        return node # line 145
Type "N@NodeTree | I@NodeTree | N@Node | I@Node | Unknown | NodeItem[P@NodeItem, I@NodeItem, N@NodeItem, T@NodeItem]* | NodeItem[Unknown, Unknown, Unknown, Unknown]*" is not assignable to return type "N@NodeTree | I@NodeTree"
  Type "N@NodeTree | I@NodeTree | N@Node | I@Node | Unknown | NodeItem[P@NodeItem, I@NodeItem, N@NodeItem, T@NodeItem]* | NodeItem[Unknown, Unknown, Unknown, Unknown]*" is not assignable to type "N@NodeTree | I@NodeTree"
    Type "I@Node" is not assignable to type "N@NodeTree | I@NodeTree"
      Type "I@Node" is not assignable to type "I@NodeTree"
      Type "I@Node" is not assignable to type "N@NodeTree"
generics python-typing