One way to fix Python circular imports

Tuesday 28 May 2024

In Python, a circular import is when two files each try to import the other, causing a failure when a module isn’t fully initialized. The best way to fix this situation is to organize your code in layers so that the importing relationships naturally flow in just one direction. But sometimes it works to simply change the style of import statement you use. I’ll show you.

Let’s say you have these files:

1# one.py
2from two import func_two
3
4def func_one():
5    func_two()
1# two.py
2from one import func_one
3
4def do_work():
5    func_one()
6
7def func_two():
8    print("Hello, world!")
1# main.py
2from two import do_work
3do_work()

If we run main.py, we get this:

% python main.py
Traceback (most recent call last):
  File "main.py", line 2, in <module>
    from two import do_work
  File "two.py", line 2, in <module>
    from one import func_one
  File "one.py", line 2, in <module>
    from two import func_two
ImportError: cannot import name 'func_two' from partially initialized
  module 'two' (most likely due to a circular import) (two.py)

When Python imports a module, it executes the file line by line. Every global in the file (top-level name including functions and classes) becomes an attribute on the module object being constructed. In two.py, we import from one.py at line 2. At that moment, the two module has been created, but it has no attributes yet because nothing has been defined yet. It will eventually have do_work and func_two, but we haven’t executed those def statements yet, so they don’t exist. Like a function call, when the import statement is run, it begins executing the imported file, and doesn’t come back to the current file until the import is done.

The import of one.py starts, and its line 2 tries to get a name from the two module. As we just said, the two module exists, but has no names defined yet. That gives us the error.

Instead of importing names from modules, we can import whole modules instead. All we do is change the form of the imports, and how we reference the functions from the imported modules, like this:

1# one.py
2import two              # was:  from two import func_two
3
4def func_one():
5    two.func_two()      # was:  func_two()
1# two.py
2import one              # was:  from one import func_one
3
4def do_work():
5    one.func_one()      # was:  func_one()
6
7def func_two():
8    print("Hello, world!")
1# main.py
2from two import do_work
3do_work()

Running the fixed code, we get this:

% python main.py
Hello, world!

It works because two.py imports one at line 2, and then one.py imports two at its line 2. That works just fine, because the two module exists. It’s still empty like it was before the fix, but now we aren’t trying to find a name in it during the import. Once all of the imports are done, the one and two modules both have all their names defined, and we can access them from inside our functions.

The key idea here is that “from two import func_two” tries to find func_two during the import, before it exists. Deferring the name lookup to the body of the function by using “import two” lets all of the modules get themselves fully initialized before we try to use them, avoiding the circular import error.

As I mentioned at the top, the best way to fix circular imports is to structure your code so that modules don’t have mutual dependencies like this. But that isn’t always easy, and this can buy you a little time to get your code working again.

Comments

[gravatar]

I like it! Simple and usefully

[gravatar]

Thanks a lot

Add a comment:

Ignore this:
Leave this empty:
Name is required. Either email or web are required. Email won't be displayed and I won't spam you. Your web site won't be indexed by search engines.
Don't put anything here:
Leave this empty:
Comment text is Markdown.