pythonpython-typingcircular-dependency

Managing Circular Dependencies and Type Hints Between Mutually Referencing Classes in Python


I have a scenario where I'm working with two classes, A and B, located in separate modules a.py and b.py. Class A uses class B as a type hint, and class B uses class A in its functionality (Association relationship). Currently, my code achieves this functionality, but I'm open to suggestions for improving or simplifying the implementation due to concerns about circular dependencies and maintainability. How can I design this interaction between classes A and B in a cleaner and more organized way while still using type hints effectively?

I currently handle this problem by something like this:

a.py:

from b import B

class A:
    def __init__(self, test: B) -> None:
        self.test = test

    def process(self):
        return self.test.hi()

b.py:

class B:
    def __init__(self) -> None:
        print(1)

    def hi(self):
        print("hi")

    def import_A(self):
        from a import A
        t = A(self)
        print(t.process())

Note that if I move from a import A to the top, a circular import error occurs. Is there any way to import everything at the beginning without causing any error?


Solution

  • A few suggestions

    don't pull A into b at all

        def import_A(self):
            from a import A
            t = A(self)
            print(t.process())
    

    this logic doesn't really have anything to do with B, and could be written in another file.

    avoid concrete imports

    from b import B
            from a import A
    

    When you import a specific value from a file, it forces python to resolve that immediately. If you instead say

    import x
      ...
        foobar(x.X)
    

    then you have a chance of allowing some "circular" imports surviving. Try to avoid using this excessively, it is still a code smell and likely means something isn't quite right in the design.

    put them in the same file

    If two classes are really intertwined, it can often be much more preferable to put them together than apart, even if it makes some files rather large. Consider Tree and Leaf node classes that have different implementations but definitely should reside together.

    use common files above and below

    Pulling abstractions into common files that both can import from can often help remove issues, this can be combined with "put them in the same file" to have abstractions together and concrete implementations separate.

    Likewise, putting the execution or test portions in a separate file means that you also avoid loops in the full logic.

    a --\           c           a   b         c
      \-- b    vs  / \    vs     \ /    vs   / \
                  a   b           e         a   b
                                             \ /
                                              e