4

I have a problem concerning recursive import statements in a package. I saw plenty of posts on the subject, but I was not able to find a working solution. I reproduced a very minimal example isolating my problem.

The file organization in the package temp is the following:

|-- __init__.py
|-- abstract.py
|-- a.py
|-- b.py

The file __init__.py contains

from .a import A
from .b import B

The file abstract.py contains

from abc import ABC, abstractmethod

class Abstract(ABC):

    @abstractmethod
    def __init__(self):
        pass

The file a.py contains

from .abstract import Abstract
from .b import B

class A(Abstract):

    def __init__(self, string):
        super().__init__()
        self.string = string

    def as_b(self):
        return B(int(self.string))

The file b.py contains

from .abstract import Abstract
from .a import A

class B(Abstract):

    def __init__(self, integer):
        super().__init__()
        self.integer = integer

    def as_a(self):
        return A(str(self.integer))

I then created a file foo.py to test the temp package which contains

from temp import A, B

an_a = A("2")
a_b = B(4)

an_a_as_b = A.as_b()
a_b_as_a = B.as_a()

At the run time, I receive the error ImportError: cannot import name 'A'. My understanding is that this is due to the recursive import statement (class A importing class B and vice-versa).

What is the best pythonic way to implement the classes A and B in the temp package?

petezurich
  • 9,280
  • 9
  • 43
  • 57
J.P. Le Cavalier
  • 1,315
  • 7
  • 16
  • would it help to move the import of "Abstract" to '_ _init_ _.py' ? – Jörg Beyer Apr 17 '19 at 19:51
  • @JörgBeyer - Nop, I just tried it and still get the same error. Furthermore, I don't want to include `Abstract` in the available classes for the user. – J.P. Le Cavalier Apr 17 '19 at 19:54
  • 1
    Easy way out: define A and B in the same file. – Tom Lubenow Apr 17 '19 at 19:55
  • @TomLubenow - This is what I did to solve my original problem, I was curious to know if there was a way to have separate files. We have a convention at work that strongly suggest to only have one class defined in any file. – J.P. Le Cavalier Apr 17 '19 at 19:58
  • People may disagree with me, but I consider the Pythonic solution to be not to have cyclic dependencies. I would design differently. If two classes can transform themselves into each other seamlessly, it seems more likely that they should just be one class that supports two interfaces. – Tom Lubenow Apr 17 '19 at 20:03
  • 1
    [This answer](https://stackoverflow.com/a/17407169/9609843) lists three possible ways to handle circular imports. My choice is the first: put import statement to the bottom of file, easiest and clearest of these three imho. – sanyassh Apr 17 '19 at 20:20
  • 1
    You can always combine `a.py` and `b.py` into a single file. – juanpa.arrivillaga Apr 17 '19 at 20:42

1 Answers1

3

There are several options besides merging the files, which you've said your workplace convention discourages. (The options below are adapted for Python 3 relative from the answer linked in this comment from Sanyash. I thought it made sense to include an answer here since that question is not directly about relative imports.)

Move the circular imports to the end of their files

The file a.py becomes:

from .abstract import Abstract

class A(Abstract):
    # ...

from .b import B

and b.py changes the same way. This is a simple change, but has the disadvantage that your imports are scattered. Someone might "clean up" your code by moving the imports to the top, and run into the same confusion you have. (You could leave a comment, but people often miss comments.)

Import the modules rather than the classes

You can also have each module import the other module rather than import from it. a.py becomes:

from .abstract import Abstract
from . import b

class A(Abstract):
    # ...
    def as_b(self):
        return b.B(int(self.string)) # note the change to use b.B

and of course b.py changes similarly. This strikes me as the cleanest solution - I strongly prefer to keep imports together when possible. (I can't think of any disadvantages to this approach, but if anyone does know of any, please leave a comment and I'll update this.)

Move the imports to where they're used

If you're only referring to B from a.py in one place like this you can move the import into the function where it's needed:

from .abstract import Abstract

class A(Abstract):
    # ...

    def as_b(self):
        from .b import B
        return B(int(self.string))

and the same for b.py. This has the advantage over the first option that it's easier to see why the import is not at the top, and I feel it's less likely to lead to confusion or bugs down the road. It can mask import errors, however, so I don't think it's ideal. It will also slightly slow the first call of as_a and as_b, since the imports will happen the first time each function is called and take nonzero time.

Nathan Vērzemnieks
  • 5,495
  • 1
  • 11
  • 23