Intermediate

As a Python developer, you must have had an encounter with the following code. If you do not yet know what is for, or why where does __name__ comes from, this guide will try to explain that, and more!

if __name__ == '__main__':
  // do something
  print("Hello, John")

To understand the need for this code, we need to delve into the python module system, special variables, and the execution of Python applications.

Python’s module system

Python, like any other programming files, allows us to split code into multiple files. Then, we can import other files, when we need code present in those files. In the python world, each file is a module, with its defined variables, classes, or other imports.

For example, let us create a simple file named triangles.py with a class Triangle and a function area:

class Triangle:
    def __init__(self, width, height):
        self.width = width
        self.height = height

def area(tri):
    return tri.width * tri.height / 2

In an interactive python terminal, we can import the triangles.py module and inspect its contents with the builtin dir function. We can see that both Triangle and area are exported:

>>> import triangles
>>> dir(triangles)
['Triangle', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'area']
>>> t = triangles.Triangle(1, 2)
>>> triangles.area(t)
1.0

We can also import files in sub-directories. For example, if the file triangles.py was inside a directory geometry, we could import it with import geometry.triangles instead:

>>> import geometry.triangles
>>> dir(geometry.triangles)
['Triangle', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'area']

__name__ and __package__ Python Variables

You must already see that the special variables __name__ and __package__ are in the exports of the triangles module. And indeed, you can inspect their value:

>>> # for the triangles.py module
>>> import triangles
>>> triangles.__name__
'triangles'
>>> triangles.__package__
''
>>> # for the geometry/triangles.py module
>>> import geometry.triangles
>>> geometry.triangles.__name__
'geometry.triangles'
>>> geometry.triangles.__package__
'geometry'
>>> # for any other module, for example numpy
>>> import numpy
>>> numpy.__name__
'numpy'
>>> numpy.__package__
'numpy'

Both variables are defined for each module at import and can be read by the code inside the module. So, if we add a function the file triangles.py that prints the __name__ variable, we can check what is the value inside the module:

class Triangle:
    def __init__(self, width, height):
        self.width = width
        self.height = height

def area(tri):
    return tri.width * tri.height / 2

def read_name():
    print(f"the __name__ is '{__name__}'")
>>> import triangles
>>> triangles.read_name():
the __name__ is 'triangles'

We can reach two conclusions from this little experiment:

  • Both __name__ and __package__ variables are unique to each python file.
  • Its value is determined when the module is imported.
  • __name__ corresponds to the relative path of the file.
  • __package__ corresponds to the relative path of the directory where the file is stored.
  • The backslashes are replaced by dots. For example, a file located in the directory a/b/c has the __package__ variable equal to ‘a.b.c‘.

The special case for __main__

Python does not define an explicit entry-point on a module. For example, in languages such as C++, Rust or Go, we must define the main function which is when the application is run. Instead, Python runs the whole file and defines the __name__ variable especially as __main__ for the file which is executed. This is such that developers customize the file to run differently whether the file is loaded or executed.

To illustrate this behavior, let’s create a file app.py which output depending on the __name__ variable.

if __name__ == '__main__':
    print("I'm being executed!")
else:
    print("I'm being imported!")
$ python app.py
I'm being executed!
$ python -c "import app"
I'm being imported!

Why is __name__ so important in Python?

While this looks pretty interesting, why should you care about the value of __name__? It depends on the kind of project you are developing. Its main goal is for developers to have executable code that can be imported, just like any library. The challenge here is to detect whether the library is being imported, or not, to determine if the executable portion is to be run. This is where the value of __name__ comes in.

From the user side, the experience is straightforward. Let’s look at the http.server package, which has this behavior:

  • For any user who wants to serve static files in a directory, it seems quite an effort to write a script to serve those files. So, the http.server library can be executed with the command python -m http.server. This is not only straightforward but very useful for website developers that don’t even have to write python code.
  • For application or server developers, the http.server is a very useful library to create an HTTP endpoint. The library exposes several classes, such as the HTTPServer, which can be included in any Python project which requires networking capabilities.

To understand how can we create such packages ourselves, let’s write a Python package to benchmark functions. That is, measure the time it takes to execute a function, given a number of parameters.

Let’s begin to create a new package called benchmarks with the Python poetry tool:

$ poetry new --name benchmarks benchmarks_py
Created package benchmarks in benchmarks_py

If you are not familiar with poetry, it’s a tool to develop python packages in a very simple way. Find more at the official website.

We will create a new file benchmark.py in the directory benchmarks inside the package, with the following content.

import time

def benchmark(fn, num_calls, *args):
    """measure the execution time of any given function in seconds"""
    tick = time.perf_counter().    #start counter
    for _ in range(num_calls):
        fn(*args)                 #Call the give function "num_call" times and get the average
    tack = time.perf_counter()
    return (tack - tick) / num_calls


if __name__ == '__main__':
    import argparse

    parser = argparse.ArgumentParser()
    parser.add_argument('--num-calls', type=int, default=1000)
    parser.add_argument('--module', type=str, required=True)
    parser.add_argument('--function', type=str, required=True)
    parser.add_argument('--args', nargs='*', type=int, default=[])
    args = parser.parse_args(). #Check arguments

    module = __import__(args.module).  
    fn = getattr(module, args.function). #dynamically load function
    fn_args = args.args
    num_calls = args.num_calls

    bench = benchmark(fn, num_calls, *fn_args). #get average runtime

    print("benchmark = ", bench)

This module exports a benchmark function, and if executed, it will load a function given by the cli arguments –module and –function, and execute it the number of calls given by –num-calls (1000 times by default).

You can see how the argparse module works in our other article on argparse

We can now build and install the package in our system with the command:

$ poetry install
$ pip install dist/benchmarks-0.1.0-py3-none-any.whl

Now, our library can be used everywhere on our system to benchmark Python functions.

As an example, let’s evaluate the performance of a simple function count_integers that sums the numbers from a to b:

def count_integers(a, b):
    sum = 0
    while a <= b:
        sum += a
        a += 1
    return sum

Using the benchmark module as a library is as simple as importing the benchmark function. Note that, because the python module is imported, the code under the if is not executed:

>>> from benchmarks.benchmark import benchmark
>>> from count_integers import count_integers
>>> benchmark(count_integers, 1000, 0, 10000)
0.0007695084930019221

We can also run the python module benchmarks.benchmark and define the arguments to run the count_integers function as well. This time, the if block is executed.

$ python -m benchmarks.benchmark \
                      --module count_integers \
                      --function count_integers \
                      --args 0 10000 \
                      --num-calls 1000
benchmark =  0.0007418607939907815

Conclusion

I hope all of you understand a bit more about the python module system, and why is __name__ used.

Get Notified Automatically Of New Articles

Error SendFox Connection: 403 Forbidden

403 Forbidden