100 Days of Learning: Day 22 – Understanding the Python importing process

Here is my Log book entry.

Code for today.

It has been bugging me for a while now that I seem to hit a Python module / package loading issue just when I think I know how it works. Time to learn and explore just how the basics of the loading system works in Python (version 3 and above).

What are modules and packages?

A module is any .py source code file. The name of the module is the file name without the extension. For example: the file hello.py is a module and the module name is hello.

A package can be any directory. Before Python 3.3 a directory must have a file named __init__.py to be seen as a package. The package name is the directory name.

What is importing?

Importing is the process by which Python code in one module is made available to Python code in another module.

An importer is an object that does both finding and loading of modules.

Single module example

Let us build up our understanding of how this importing process works by first starting with just a single module.

The project’s root directory is just called project in this example. It contains a file named example.py. Thus this means we have a module named "example".

# Directory structure
project
    └── example.py
# example.py

print(f'This is global scope of example.py. __name__ is: {__name__}')

if __name__ == '__main__':
    print('This is example.py running')
    print(f'My __name__ is: {__name__}')

Let’s see what happens when we tell the interpreter to load our module.

$ python example.py
This is global scope of example.py. __name__ is: __main__
This is example.py running
My __name__ is: __main__

$ python -m example
# Gives the same result

Ok so we can see that when the python interpreter loaded the module it set the __name__ variable to __main__. It first ran all the code at the global scope.

We then have an if check to see if the __name__ is __main__ and if so then run more code.

Module importing another module

Add another file named module1.py inside the same directory.

# module1.py

print(f'This is global scope of module1.py. __name__ is: {__name__}')

if __name__ == '__main__':
    print('This is module1.py running')
project
    ├── example.py
    └── module1.py

Load and run this new module.

$ python module1.py
This is global scope of module1.py. __name__ is: __main__
This is module1.py running

Modify example.py to import module1 and load example.py.

# example.py

print(f'This is global scope of example.py. __name__ is: {__name__}')

import module1

if __name__ == '__main__':
    print('This is example.py running')
    print(f'My __name__ is: {__name__}')
$ python example.py
This is global scope of example.py. __name__ is: __main__
This is global scope of module1.py. __name__ is: module1
This is example.py running
My __name__ is: __main__

Notice how the value of __name__ has been changed to module1 when the code from module1.py was loaded and executed as the result of being loaded by another module.

Also take notice that in this case the if __name__ ... code from module1 was not run (as you would expect).

Key point: The name of the module first loaded and executed by the Python interpreter will be set to __main__ regardless of what the module’s actual name is.

Single package example

Create a new directory named package1 and add a file named __init__.py

project
    ├── example.py
    ├── module1.py
    └── package1
        ├── __init__.py
# package1 __init__.py
print(f'This is global scope of package1/__init__.py. __name__ is: {__name__}')
print(f'__package__ is: {__package__}')

Modify example.py

...
print(f'This is global scope of example.py. __name__ is: {__name__}')
print(f'example.py __package__ is: {__package__}')

import module1
import package1
...

Modify module1.py

...
print(f'This is global scope of module1.py. __name__ is: {__name__}')
print(f'module1.py __package__ is: {__package__}')
...

Load and run example.py

$ python example.py
This is global scope of example.py. __name__ is: __main__
example.py __package__ is: None
This is global scope of module1.py. __name__ is: module1
module1.py __package__ is:
This is global scope of package1/__init__.py. __name__ is: package1
__package__ is: package1
This is example.py running
My __name__ is: __main__

Notice how the package name of example.py is None whereas the package name for module1.py is blank (I am guessing this is an empty string ”).

example.py imported package1 and the loading system found the file named __init__.py and executed the code. The name of the package is what we would expect (the name of the directory i.e. package1).

What happens if we used python -m to load and run example.py?

$ python -m example
This is global scope of example.py. __name__ is: __main__
example.py __package__ is:
This is global scope of module1.py. __name__ is: module1
module1.py __package__ is:
This is global scope of package1/__init__.py. __name__ is: package1
__package__ is: package1
This is example.py running
My __name__ is: __main__

Can you spot the difference?

example.py ‘s __package__ is set to None when you use python example.py where as it is blank when you use the -m option to load the module named example.

Wrap up for tonight

I have barely even started scratching the surface of this topic.

Tomorrow I will be diving more into how this module loading process works in Python.