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?

Updated: