Prevent cyclical import when type hinting custom exception __init__ method

Hi, I’m trying to create a tree data structure in python to do this I have the following files:

# module/node.py

from collections.abc import Iterable
from module.exceptions import MultipleParentsError

class Node:
    def __init__(self, initial_children: Iterable[Node] = ()) -> None:
        self.parent: Node | None = None
        
        self.children: list[Node] = []
        for child in initial_children:
            self.add_child(child)
 
     def add_child(self, child):
          if child.parent is not None:
              raise MultipleParentsError(child, self, child.parent)
          else:
              self.children.append(child)
              child.parent = self

and

# module/exceptions.py
 
from module.node import Node

class MultipleParentsError(RuntimeError):
    def __init__(self, child: Node, new_parent: Node, preexisting_parent: Node, *args: object) -> None:
        super().__init__(f"Tried to add child {child} to {new_parent} but child already has a parent: {preexisting_parent}", *args)

I only import Node in the exceptions.py for type checking, The problem is that this creates an import cycle. From what I could see by searching online, the variable typing.TYPE_CHECKING can be used to avoid this, however my type checker (basedpyright) still shows the import cycle error. This is apparently because this type of error is indicative of a larger issue with the program’s structure, I can see two solutions to this, but I don’t know which one is the best or if I am missing one:

  1. Move MultipleParentsError to the node.py file as it is related to the same functionality (so far this is my preferred solution)
  2. Give up and use type Any or no type hinting (but this seems like a bad idea)
  1. use from module import node and node.Node in annotation

Can you share what your code looks like when you do this? From the snippets you’ve provided, typing.TYPE_CHECKING should be perfectly suitable, so I think there must be some context you’re leaving out.

That doesn’t help as that still imports node.py which imports exceptions.py

It would look like this:

# module/exceptions.py
 

from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from module.node import Node

class MultipleParentsError(RuntimeError):
    def __init__(self, child: Node, new_parent: Node, preexisting_parent: Node, *args: object) -> None:
        super().__init__(f"Tried to add child {child} to {new_parent} but child already has a parent: {preexisting_parent}", *args)


I still get the following error: Cycle detected in import chain basedpyright (reportImportCycles)

this is apparently intended by the developer: see here

From a runtime and type safety standpoint, there’s nothing wrong with this code. This is a case where your type checker is being opinionated in a way that you may not agree with. I would personally just disable this error code, as it is an optional one.

OK, I was worried it meant there was an issue with the structure of my code, but if you think it’s fine then I’ll just disable the error