Our First Module

Let's create our first module and learn step-by-step how to build it.

Step 1: Creating an Empty Module, module.py

Python Package Structure

Working locally on your machine, start from scratch by creating an empty file named module.py for the first part of the experiment. This file will serve as the module, and even though it's empty now, you will soon fill it with actual code. The straightforward name, module.py, is chosen for simplicity and clarity.


Step 2: Importing module.py

Then, create second file named as main.py in the same folder contains the code module.py that uses the new module (import module). To get started, it's best to create an empty new folder and place both files in it.

Next, open IDLE or your preferred IDE, and run the main.py file. If everything is set up correctly, you won't see any output, which means Python has successfully imported the contents of the module.py file, even if it's currently empty.

Python Package Structure

Now, take a look at the folder where both files are located. You'll notice a new subfolder named pycache. Go inside this folder, and you'll find a file named something like module.cpython-xy.pyc, where x and y correspond to your Python version (e.g., 3 and 8 for Python 3.8).

Python Package Structure

The name of this file matches the name of your module (module in this case). The part after the first dot indicates the Python implementation that created the file (CPython in this case) and its version number. The last part, .pyc, comes from "Python" and "compiled".

You can't read the contents of this file directly because it's in a semi-compiled form intended for Python's internal use. When Python imports a module for the first time, it translates its contents into this semi-compiled shape.

This semi-compiled file doesn't contain machine code but is optimized for Python's interpreter, leading to faster execution compared to interpreting the source text from scratch. It also doesn't require as many checks as a pure source file, further contributing to its faster performance.

The Python interpreter automatically handles the process of checking if the module's source file has been modified. If it has, the .pyc file will be rebuilt; otherwise, the existing .pyc file may be used directly. This entire process is automatic and transparent to the user, so you don't need to worry about it.


Step 3: Running module.py

Now, let's type in the following code in module.py and save it:

print("**Module Function**")

If you run module.py, and you will see the following output:

OUTPUT:

Step 4: Running main.py

Then, go back to main.py file, and run main.py. You should see the same output as in Step 3.

OUTPUT:

But what does this output mean?

When a module is imported in Python, its contents are automatically executed. This gives the module an opportunity to initialize certain internal aspects, like assigning variables with useful values.

It's essential to note that the initialization happens only once, during the first import. So, if a module is imported multiple times, Python remembers the imported modules and skips redundant imports silently.

Example

For example, in the given context:

  • There is a module named modA.
  • There is another module named modB, which contains the instruction import modA.
  • There is a main file that includes the instructions import modA and import modB.

Even though it may seem like modA would be imported twice (once directly and once through modB), Python ensures that the first import takes place only, and any subsequent imports of the same module are ignored.

Python Package Structure

Step 5: Understanding name Variable

In addition to importing modules, Python also creates a variable called name. This variable is unique to each source file and is not shared between modules. We will demonstrate how to utilize this variable by making some modifications to the module.

Now, add the following code in module.py, save and run it. You will see the output as shown below.

print("**Module Function**")
print(__name__)
OUTPUT:

Then, in main.py, add the following code save and run it. You will see the output as shown below.

import module

print("This is main function.")
OUTPUT:

To conclude, in Python, there's a special variable called __name__ that behaves differently depending on how you run a file. If you run the file directly, the __name__ variable is set to __main__. On the other hand, if you import the file as a module in another script, the __name__ variable is set to the file's name (without the .py extension).


Step 6: Leveraging the __main__ Variable

You can utilize the __main__ variable to distinguish between running the code as a module or directly. Here's how:

Modify the existing code in module.py as displayed below, save and run it. You will see the output as shown below.

if __name__ == "__main__":
    print("Hello! I am a module. Simply run me as a module.")
else:
    print("**Module**")
OUTPUT:

This clever technique allows you to place tests within the module to check the functions' correctness. When running the module directly, the tests will be executed, providing a quick way to verify recent changes without affecting the module's behavior when imported.

For a clear view, let's try run the main.py and see what happen. Below is the code contain in main.py, we are not changing anything. Try to run it.

import module

print("This is main function.")
OUTPUT:

To conclude, the __main__ variable provides a powerful way to distinguish between running code as a module or directly. By incorporating tests within the module and running it directly, you can efficiently verify recent changes without impacting the module's functionality when imported. This technique facilitates easy and effective code testing and execution, enhancing the development process.


Step 7: Managing Module Functions and a Variable

Now, assume that we want to keep track of how many times these functions are invoked. And we modify the code in module.py as per shown below. In the shown code, to keep track of how many times these functions are invoked, we use a counter by declaring a variable named counter. So, when the module is imported, the counter should be initialized to zero.

counter = 0
if __name__ == "__main__":
    print("Hello! I am a module. Simply run me as a module.")
else:
    print("**Module**")

However, introducing such a variable is absolutely correct, but may cause important side effects that you must be aware of. Let's try to display the counter value in main.py. Modify the code in main.py as per shown below, save it and run it and observe the output.

import module
print("This is main function.")
print(module.counter)
OUTPUT:

In Python, the main file can access a module's counter variable, which is legal and potentially very useful. However, it's important to consider safety. If you trust your module's users, there's no issue. But if you don't want others to access your personal/private variable, you can use a naming convention to indicate that it's intended for internal use only. You can use a single underscore _ or double underscore __ before the variable name, though it's only a convention and users may choose to disregard it.

Now, let's add two functions to the module for evaluating the sum and product of numbers in a list, while ensuring unnecessary remnants are removed.


Python Module: Importing Modules from Different Directories

Step 1: Python Module: Functions for Sum and Product Operations

Now, let's write some brand new code in our module.py file. The updated module is as shown below:

#!/usr/bin/env python3

"""module.py - an example of a Python module"""

__counter = 0

def suml(the_list):
  global __counter
  __counter += 1
  the_sum = 0
  for element in the_list:
    the_sum += element
  return the_sum

def prodl(the_list):
  global __counter
  __counter += 1
  prod = 1
  for element in the_list:
    prod *= element
  return prod

if __name__ == "__main__":
  print("I prefer to be a module, but I can do some tests for you.")
  my_list = [i+1 for i in range(5)]
  print(my_list)
  print(suml(my_list) == 15)
  print(prodl(my_list) == 120)

Let's go over them:

  • The line starting with #! is known as the "shebang" or "hashbang," which instructs Unix and Unix-like OSs on how to execute the file. For Python, it's just a comment as it starts with #, but in some environments (like web servers), its absence can cause issues.
  • The text placed in triple quotes before any module instructions is called the "doc-string". It provides a brief explanation of the module's purpose and contents.
  • The functions suml() and prodl() defined inside the module are available for import, allowing you to utilize their functionality in other scripts.
  • By using the __name__ variable, we can detect when the file is run as a standalone program. In this case, we perform some simple tests to ensure the module is working as expected.

**Note: The "shebang" is the combination of characters "#!" at the beginning of a script file. It's used in Unix-like operating systems to tell the system which interpreter to use for executing the script. It allows you to run scripts directly from the command line without explicitly specifying the interpreter.

All about __counter:

  • Purpose: The __counter variable is used to keep track of how many times the module has been imported. It serves as a counter that increments each time the module is imported in another script.
  • __ (Double Underscore) Prefix: The __counter variable has a double underscore prefix, which makes it a private variable in Python. This naming convention indicates that the variable is intended for internal use within the module and is not intended to be accessed directly from outside the module.
  • Global Declaration: Inside both the suml and prodl functions, the __counter variable is declared as global. This declaration allows the functions to modify the value of the __counter variable, even though it is defined outside of the functions' scope.

How to access __counter variable when importing the module from main.py?

  • When you import the module.py module into another script, such as main.py, you won't be able to access the __counter variable directly. As it is a private variable with a double underscore prefix, it's intended for internal use within the module only.
  • However, you can still access and interact with the __counter variable indirectly by using functions defined in the module that modify the counter. In this case, you can call the suml or prodl functions, and they will increment the __counter variable each time they are called.
  • Here's an example of how to access the __counter variable when importing the module.py module into main.py:
#main.py - Import the module
import module

#Sample lists
my_list = [i + 1 for i in range(5)]

#Call the functions to interact with the __counter variable
module.suml(my_list)
module.prodl(my_list)

  • Now, the __counter variable has been incremented by 2 (from the two function calls).
  • However, you still can't access __counter directly like module.__counter
print(module.__counter) # This will result in an AttributeError as __counter is private

  • The __counter variable is not accessible directly through the module when imported because of its private nature. The recommended way to interact with it is through the functions provided by the module that increment the counter as needed.
  • This encapsulation of the __counter variable helps maintain a level of abstraction, preventing unintended modification of internal variables by external code. It's a common practice in Python to use a single or double underscore prefix for variables intended for internal use within a module or class.

Let's see the output when running module.py:

OUTPUT:

Now, let's see how we can use the functions from module.py in main.py. Simply make the following modifications to the code in main.py:

from module import suml, prodl

zeroes = [0 for i in range(5)]
ones = [1 for i in range(5)]
print(suml(zeroes))
print(prodl(ones))
OUTPUT:

Step 2: Understanding Module Import in Different Locations

Let's make our example more interesting by considering a scenario where the main Python file and the module to be imported are in different locations. We'll assume the main Python script, named main.py, is situated in C:\Users\user\py\progs, and the module (module.py) is in C:\Users\user\py\modules. Keep in mind that this example assumes we are using the Windows ® OS, as the file name's structure depends on it.

To understand how Python searches for modules, we need to know about a special variable called sys.path. It's actually a list that stores the locations (folders/directories) Python searches to find a requested module through the import instruction.

Python looks through these folders in the order they appear in the list. If it can't find the module in any of these directories, the import will fail. Otherwise, the first folder with the desired module will be used (if other folders have a module with the same name, they will be ignored).

To check the regular value of sys.path, you can use the following code:

import sys
for p in sys.path:
  print(p)

For example, when running this code from the C:\User\user folder, the output could be:

OUTPUT:

Now, if you want to solve the problem of not finding a module, you can add the folder containing the module to the sys.path variable. This way, Python will search in the added folder, and the import should work correctly. Keep in mind that sys.path is modifiable, allowing you to customize the search path.

By adding the relevant folder to sys.path, Python will be able to find and import the desired module successfully.


Step 3: Importing Modules from Different Directories

To utilize a module located in a different directory, follow these steps in your main.py:

  1. Import the module by updating the system path using the path module from sys.
  2. Append the path of the directory containing the module using path.append('..\\modules').
  3. Now, you can import the desired module using import module.

Here's an example:

# main.py

# Step 1: Import the path module and append the directory path
from sys import path
path.append('..\\modules')

# Step 2: Import the module
import module

# Sample lists
zeroes = [0 for i in range(5)]
ones = [1 for i in range(5)]

# Step 3: Use the functions from the imported module
print(module.suml(zeroes))
print(module.prodl(ones))

Note:

  • We doubled the backslashes (\\) in the directory path because a single backslash is used for escaping characters. To represent a single backslash, we escape it as well.
  • The relative path used in path.append('..\\modules') will work if you execute main.py directly from its home folder. If the current directory doesn't match the relative path, you can use an absolute path like path.append('C:\\Users\\user\\py\\modules').
  • We used append() to add the new path to the end of the path list. If you prefer to add the new path at a specific position, you can use insert() instead.