Python program importing itself

Question:

I made a file named challenge.py with this code in it:

import challenge

def main():
     print('Circular much...')

challenge.main()

From this I was expecting python to raise an error due to the circular import of importing the file which is running but I found that on python 3.7 & 3.8 this file runs and prints out Circular much... twice. I would understand once as that would mean that the rest of the file when it is importing itself isn’t running and I would understand a recursion error as it ran challenge.main() infinitely down the stack but I don’t understand why it prints it twice and stops?

Asked By: 13ros27

||

Answers:

Tracing this through:

import challenge

Ok, we’ll import challenge.py. Here we go…

  • import challenge
    

    We’re already importing challenge.py, so we won’t do it again.

    def main():
         print('Circular much...')
    

    Defined the function main() in the namespace challenge. Cool.

    challenge.main()
    

    Now call the function main() in the namespace challenge. That prints Circular much…. There’s your first print.

Now we’re back in the main module again.

def main():
     print('Circular much...')

This defines the function main() in the global namespace (which never gets called).

challenge.main()

This calls the function main() in the namespace challenge, again printing Circular much….

And we’re done. Two prints of your message.

Answered By: Fred Larson

It may be instructive to look at sys.modules.

For example:

import sys

def check_modules():
     print("seeing what we have...")
     if "challenge" in sys.modules:

          module = sys.modules["challenge"]
          
          print(f"sys.modules contains {module.__file__}")
          
          if hasattr(module, "challenge"):
               print("... and it contains the variable 'challenge'")
               if module is module.challenge:
                    print("... pointing back at itself")
                    
          if hasattr(module, "main"):
               print("... and it contains the variable 'main'")
               
     print()

print("Before import:")
check_modules()

import challenge

print("After import:")
check_modules()

def main():
     print('Circular much...')

print("After declaring 'main':")
check_modules()
     
challenge.main()

gives:

Before import:
seeing what we have...

Before import:
seeing what we have...
sys.modules contains /tmp/challenge.py

After import:
seeing what we have...
sys.modules contains /tmp/challenge.py
... and it contains the variable 'challenge'
... pointing back at itself

After declaring 'main':
seeing what we have...
sys.modules contains /tmp/challenge.py
... and it contains the variable 'challenge'
... pointing back at itself
... and it contains the variable 'main'

Circular much...
After import:
seeing what we have...
sys.modules contains /tmp/challenge.py
... and it contains the variable 'challenge'
... pointing back at itself
... and it contains the variable 'main'

After declaring 'main':
seeing what we have...
sys.modules contains /tmp/challenge.py
... and it contains the variable 'challenge'
... pointing back at itself
... and it contains the variable 'main'

Circular much...

As you can see, the module gets added to sys.modules at the start of the import, before the actual code in the module being imported is run. If the import statement is reached when the module file is present in sys.modules, then this is sufficient to prevent it being imported again, which is why there is only a single level of recursion.

Once the import is finished, the result of the import (a module object) is assigned to the variable challenge, and the if module is module.challenge test in the code confirms that it is a reference to the same module as the one in which the name is created (as importing a module already imported simply reuses the same module object already created).

Now as regards the question of how the call to challenge.main works: precisely because challenge is simply a reference to the current module, this means that when the function definition is executed, thereby creating the name main inside the current module object, the same function object to which it points can equally be accessed as challenge.main instead of main.

Answered By: alani

I was expecting python to raise an error due to the circular import of
importing the file which is running

Now that folks have explained why it didn’t work, let’s consider what it takes to make it work. Perhaps:

from sys import argv

print('Circular much...')

exec(open(argv[0]).read())

OUTPUT

% python3 challenge.py
Circular much...
Circular much...
Circular much...
Circular much...
Circular much...
... # 489 more recursions later
Circular much...
Circular much...
Circular much...
Circular much...
Circular much...
Traceback (most recent call last):
  File "challenge.py", line 5, in <module>
    exec(open(argv[0]).read())
  File "<string>", line 5, in <module>
  File "<string>", line 5, in <module>
  File "<string>", line 5, in <module>
  [Previous line repeated 494 more times]
  File "<string>", line 3, in <module>
RecursionError: maximum recursion depth exceeded while calling a Python object
% 
Answered By: cdlane

It is only running twice: once because the file was ran and again because the file is imported. The second time the line ‘import challenge’ is read, it does not recursively import forever because it was already imported into this namespace, which prevents it from importing a second time. Just like how importing a module twice in the same script will only import the module once.

Answered By: Godfreyluck