Python __init__.py – Best Practices and Customizations

The __init__.py file is a special Python script that is executed when a package or module is imported. Its primary purpose is to initialize the package or module and define the package’s namespace. In this tutorial we will discuss various best practices and customizations involving the __init__.py file that you can do when creating your own packages.



When to use __init__.py?

Before we continue any further, let us build some intuition on why the __init__.py is needed, and when to use it.

Scenario: We have a large scale application, which involves 50+ different Python files, each file representing a single class, or module within the code. All of these files look very messy within a single folder, thus we would like to group and divide them into smaller subfolders.

Here is a sample folder structure that you might try creating:

my_large_application/
│
├── models/
│   ├── file1_model.py
│   ├── file2_model.py
│   └── ...
│
├── controllers/
│   ├── file3_controller.py
│   ├── file4_controller.py
│   └── ...
│
└── main.py

But this raises another problem. How do we manage our imports? For example, how does the main.py file import the other files in the subfolders? How does a file from a subfolder (e.g. models) import a file from another subfolder (e.g. controllers)?

Let’s start with the first example, on how to import a file from a subfolder into the main.py file. The following line of code is a valid import statement that will import all of the contents of the file1_model.py into the main.py file.

from models.file1_model import *

(Don’t include the .py extension in the file name when importing)

However, with our current folder setup, this command will fail, and the following error will be thrown.

This is where the __init__.py file comes in. Right now, the folder “models” is not being treated as a package by Python. The term “package” in this context refers to a folder containing Python files and scripts. In order to have the “models” folder detected as a package from which files can be imported, we have to add an __init__.py file inside it. We don’t have to put anything inside the __init__.py file. Just create any empty file, and rename it to __init__.py.

We will also do the same for the controllers folder, so our new folder structure should like this:

my_large_application/
│

│
├── models/
│   ├── __init__.py
│   ├── file1_model.py
│   ├── file2_model.py
│   └── ...
│
├── controllers/
│   ├── __init__.py
│   ├── file3_controller.py
│   ├── file4_controller.py
│   └── ...
│

└── main.py

And now our import statements will work just fine, for both the model and controller folders.


Importing Files from a Sub-directory into another Sub-directory

Now that we know how to import files from the subfolders into main.py, lets explore how to import files between sub-folders. Don’t worry, it’s actually pretty easy.

Scenario: We want to import the contents of file2_model.py into file3_controller.py.

from models.file1_model import *

This command will fail if the __init__.py file is missing from either on these subfolders.


Do not think that __init__.py is only meant for large-scale applications. Even small scale application should keep themselves organized. Furthermore, developers who are responsible for creating or managing packages (e.g. a library) will also need to understand how to use and configure this file.


Customizing the __init__.py File

Now that we have discussed the essentials, we will explore some optional statements we can include into our __init__.py file to customize its behavior.


1. Adding Package-Level Variables

You can use the __init__.py file to define variables that are accessible throughout the package. For example:

# __init__.py

# Package-level variable
package_variable = "This is a package-level variable."

Now you can access it from another file (e.g. main.py file located in the same directory as the package):

# main.py

from your_package import package_variable

# Using the package-level variable
print(f"The value of the package variable is: {package_variable}")

2. Improving Readability of Imports

Now, let’s talk about improving the way you import modules. In the __init__.py file, you can tidy up your imports, making them more concise. In main.py, you can then directly access functions from these modules without specifying the module, creating a cleaner and more readable code structure.

# __init__.py

# Importing modules for easier access
from .module1 import function1
from .module2 import function2

Now you can directly import these functions:

# main.py

from your_package import function1, function2

# Using the functions from the modules
function1()
function2()

3. Executing Custom Code on Package Import

Sometimes, you may want to execute specific code when your package is imported. The __init__.py file provides the perfect spot for such initialization code.

# __init__.py

# Code to execute on package import
print("This will be executed when the package is imported.")

In main.py, simply importing a function from your package will trigger this code, offering a seamless way to set things up.

# main.py

from your_package import function1

# Importing a function triggers the code in __init__.py

Output (after running main.py)

This will be executed when the package is imported.

4. Package Initialization with __all__

When you want to control which modules get imported when using the from your_package import * syntax, you can use the __all__ variable in your __init__.py file. This variable takes a list of modules you wish to be available for import. This is useful in cases where you wish some modules to only be available internally (within the package).

# __init__.py

__all__ = ['module1', 'module2']

In main.py, you can then import all the specified modules easily using the * syntax:

# main.py

from your_package import *

# Using functions from module1 and module2
function1()
function2()

5. Dynamic Importing

Implement dynamic importing based on certain conditions or configurations. This can be useful for selectively importing modules or functions based on runtime conditions. This feature would be useful in scenarios such as cross-compatibility between platforms, or version differences (old python versions) requiring backward compatibility.

# __init__.py

# Dynamic importing based on a configuration
if CONFIGURATION == 'A':
    from .module_a import *
elif CONFIGURATION == 'B':
    from .module_b import *
else:
    raise ImportError("Invalid configuration")

6. Alias Module Imports

Create aliases for modules or functions, making them more user-friendly or providing backward compatibility.

# __init__.py

from . import module1 as m1
from . import module2 as m2

We can now import it from the main file as the following:

# main.py

from your_package import *

# Using functions from module1 and module2
function1()
function2()

7. Initializing External Libraries

Initialize and configure external libraries or dependencies when the package is imported. This helps encapsulate setup tasks and ensures they are executed consistently.

# __init__.py

# Initializing an external library
import external_library

external_library.setup()

Conclusion

This marks the end of the “Python __init__.py – Best Practices and Customizations” Tutorial. Any suggestions or contributions for CodersLegacy are more than welcome. Questions regarding the tutorial content can be asked in the comments section below.

Subscribe
Notify of
guest
1 Comment
Oldest
Newest Most Voted
Inline Feedbacks
View all comments