Circular Imports

I am struggling to find a readable explanation of how circular imports are handled in Python and JavaScript (more specifically, by the require function used for CommonJS modules). Here’s a short example in Python:

a.py b.py
import b

def P():
    print("P")
    b.Q()

def R():
    print("R")
    
import a

def Q():
    print("Q")
    a.R()

If we run this in the interpreter, the functions call each other as desired:

>>> import a
>>> a.P()
P
Q
R

And if we change a.py to call P() as the file is being loaded, it still works:

a.py b.py
import b

def P():
    print("P")
    b.Q()

def R():
    print("R")

P() # ADDED
    
import a

def Q():
    print("Q")
    a.R()
>>> import a
P
Q
R

But if we run a.py directly from the command line, it fails:

$ python a.py
P
Traceback (most recent call last):
  File "a.py", line 1, in <module>
    import b
  File "/home/gvwilson/example/b.py", line 1, in <module>
    import a
  File "/home/gvwilson/example/a.py", line 10, in <module>
    P()
  File "/home/gvwilson/example/a.py", line 5, in P
    b.Q()
AttributeError: module 'b' has no attribute 'Q'

Update: see this thread from EM Bray.

Node is more consistent. Suppose I have two JavaScript files a.js and b.js:

a.js b.js
const {Q} = require('./b')

const P = () => {
  console.log('P')
  Q()
}

const R = () => {
  console.log('R')
}

module.exports = {P, R}
    
const {R} = require('./a')

const Q = () => {
  console.log('Q')
  R()
}

module.exports = {Q}

Running inside the interpreter fails during loading:

> const {P} = require('./a')
undefined
> (node:1756) Warning: Accessing non-existent property 'R' of module exports inside circular dependency
(Use `node --trace-warnings ...` to show where the warning was created)
> P()
P
Q
Uncaught TypeError: R is not a function
    at Q (/home/gvwilson/example/b.js:5:3)
    at P (/home/gvwilson/example/a.js:5:3) 

and we get a similar error if we modify a.js to call P() at the top level as the file is loading. It also fails if we use a bundler like Parcel. Our HTML file is:

<html>
  <head>
    <script src="./a.js"></script>
  </head>
  <body>
    <p>Hello</p>
  </body>
</html>

and we run with:

$ parcel test.html
Server running at http://localhost:234
    Build in 0.124s.

When we look in the browser console, we see an uncaught error because R is not a function.

Does this mean that circular imports simply don’t work? That seems to be the case for JavaScript, but not necessarily for Python. My questions now are:

  1. Why do they work sometimes for Python but not always?
  2. Do they really not work in JavaScript without extra developer effort (e.g., creating a module initialization function so that loading and initializing are separated)?
  3. Most importantly, where can I find tutorials that explain how things like this work—not just for these two languages but in general? Where are the compare-and-contrasts for up-and-coming software engineers (and older ones like myself who know a lot less than we think we do)? Why are books like Levine’s Linkers and Loaders and Pearson’s Software Build Systems so rare?

In the wake of posts about Shopify's support for white nationalists and DataCamp's attempts to cover up sexual harassment
I have had to disable comments on this blog. Please email me if you'd like to get in touch.