0

I have a file structure as follows:

├── test_package
│   ├── __init__.py
│   └── models
│       ├── __init__.py
│       ├── a.py
│       └── b.py

And the following file contents:

# a.py
from .b import B
from dataclasses import dataclass

@dataclass
class A:
    b: B

# b.py
from .a import A
from dataclasses import dataclass

@dataclass
class B:
    a: A

When trying to import package a, I get the following error.

: import test_package.models.a                                                                 
---------------------------------------------------------------------------
ImportError                               Traceback (most recent call last)
<ipython-input-1-e8165f4eb507> in <module>
----> 1 import test_package.models.a

/.../test_package/models/a.py in <module>
----> 1 from .b import B
      2 from dataclasses import dataclass
      3 
      4 @dataclass
      5 class A:

/.../test_package/models/b.py in <module>
----> 1 from .a import A
      2 from dataclasses import dataclass
      3 
      4 @dataclass
      5 class B:

ImportError: cannot import name 'A' from 'test_package.models.a'

According to the Python3.5 change log, "Circular imports involving relative imports are now supported. (Contributed by Brett Cannon and Antoine Pitrou in bpo-17636.)" How do I make this relative circular import work?

Note that I only have to make these imports to do type checking.

Edit:

As a reply to @juanpa.arrivillaga in the comments, here is an example of the same error happening with Python 2.7. It seems like the source files are being found and being executed unless I'm misinterpreting something.

$ cat a.py
from b import b_func

def a_func():
    return "Hi"

$ cat b.py
from a import b_func

def b_func():
    return "Hi"

>>> import a
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "a.py", line 1, in <module>
    from b import b_func
  File "b.py", line 1, in <module>
    from a import b_func
ImportError: cannot import name b_func
Kent Shikama
  • 3,910
  • 3
  • 22
  • 55
  • This is a fundamental problem with circular dependencies. You can think of python code executing line by line, so when in `a.py`, you `from .b import B`, it goes to `b.py`, and then tries to `from .a import A`, but in `a.py`, your `class A:` has not executed, and does not exist. The easiest solution is to merge this into a single module. – juanpa.arrivillaga Nov 01 '19 at 22:36
  • What does "Circular imports involving relative imports are now supported" refer to then? – Kent Shikama Nov 01 '19 at 22:38
  • That you can do this at all, however, the actual code in your circular imports will be broken. – juanpa.arrivillaga Nov 01 '19 at 22:38
  • "That you can do this at all" - Would you mind expanding on this a bit? Does this mean it passes some stage (e.g. type checking phase) but not the next? – Kent Shikama Nov 01 '19 at 22:39
  • There is not "type checking phase". Python is a dynamically typed language. Your imports are **successful**, that is, they find the source-code file being referenced by the circular import, and begin to execute them, but as I explained, the names you are expecting to exist won't exist yet. – juanpa.arrivillaga Nov 01 '19 at 22:41
  • 1
    Also note, this is not a problem with relative imports, this would fail with non-relative imports as well. And for that matter, the fact that these are data-classes, or classes at all, is also not really relevant. – juanpa.arrivillaga Nov 01 '19 at 22:43
  • "they find the source-code file being referenced by the circular import" - wasn't this always the case even with Python 3.4? – Kent Shikama Nov 01 '19 at 22:48
  • In general, circular imports are the result of bad designs. In this case you're attempting to define a infinitely recursive data-structure. i.e. `class A` instances contain a `class B` instance that contains a `class A` instance that contains a `class B` instance that…etc. which Python doesn't support (regardless of how the attempt is being made). So, as @juanpa already pointed out, the issue has nothing to do with `dataclass`es or absolute vs relative imports per se. – martineau Nov 01 '19 at 23:13
  • Related, although the answer(s) won't support the definition of your data-structure (because it's the real issue): [Python circular importing?](https://stackoverflow.com/questions/22187279/python-circular-importing) – martineau Nov 01 '19 at 23:32
  • @martineau Is it still a bad design pattern if I'm using a weakref back? – Kent Shikama Nov 01 '19 at 23:45
  • 1
    No, I don't think that helps — the data-structure is still infinite. Hard to say for sure — because I don't know what you're ultimately trying to accomplish — but perhaps you could avoid the problem by defining a separate container class of some sort that has a finite number of these objects in it. – martineau Nov 01 '19 at 23:54

1 Answers1

2

"When a type hint contains names that have not been defined yet, that definition may be expressed as a string literal, to be resolved later." For example,

@dataclass 
class A: 
    b: 'B' 

@dataclass 
class B: 
    a: A

>>> A.__annotations__ # This is what the type checker uses
{'b': 'B'} 

See https://www.python.org/dev/peps/pep-0484/#forward-references for more information.

Note this isn't the answer to the question I asked but it is the answer I wished I had received. And so I am posting this answer in case someone goes through the same path that I did.

Kent Shikama
  • 3,910
  • 3
  • 22
  • 55