CodingNic

Debugging, Testing, and Modules

Modules Introduction

Debugging, Testing, and Modules 20 min read

Modules Introduction

Objectives

By the end of this chapter, you should be able to:

  • Define what a module is
  • Import custom and built-in modules
  • Explain the purpose of the if __name__ == "__main__" pattern
  • Organize multiple modules into a package with __init__.py

๐Ÿ’ก Why this matters: As a program grows, keeping everything in one file gets unmanageable fast. Modules are how you split code across files and reuse it wherever you need it.

Writing Your Own Module

A module is just a Python file whose code you can import elsewhere. Say you write a file called helpers.py:

python
def add(a, b):
    return a + b

def subtract(a, b):
    return a - b

From another file in the same directory, app.py, you can pull those functions in directly:

python
from helpers import add, subtract

def calculate_numbers(a, b, fn):
    if fn == "add":
        return add(a, b)
    elif fn == "subtract":
        return subtract(a, b)

calculate_numbers(1, 4, "add")  # 5 (calls add, which came from helpers.py)

Different Ways to Import

python
import random          # the whole module: call functions as random.something()
random.random()

import random as r     # the whole module, aliased to a shorter name
r.random()

from random import random   # just one function, called directly
random()

from random import random as r  # one function, aliased
r()

A few more patterns you’ll see:

python
import math as m
from math import sqrt
from my_module import only_what_i_need
from my_module import *   # everything from my_module: avoid this in real code

Convention: put your import statements at the top of the file, and prefer importing exactly what you need rather than everything. from my_module import * is especially worth avoiding: it dumps every name from that module into your file, making it hard to tell where a given name actually came from.

The if __name__ == "__main__" Pattern

__name__ is a special variable Python sets automatically. It equals "__main__" only when a file is run directly, not when it’s imported. That lets a module keep code that should run when executed on its own, but not when someone else imports it.

helper.py:

python
print("I'm from the helper file!")

def display(name):
    print("My name is " + name)

display(__name__)

if __name__ == "__main__":
    print("I'm the helper file and was loaded directly!")

Run python3 helper.py directly, and it prints all three lines, since __name__ really does equal "__main__" here:

text
I'm from the helper file!
My name is __main__
I'm the helper file and was loaded directly!

Now import it from other.py:

python
import helper

print("I'm from the other file!")
helper.display(__name__)

if __name__ == "__main__":
    print("I'm the other file and was loaded directly!")

Running other.py prints:

text
I'm from the helper file!
My name is helper
I'm from the other file!
My name is __main__
I'm the other file and was loaded directly!

Inside helper.py, __name__ is "helper" this time, not "__main__", so its if __name__ == "__main__": block never runs. Inside other.py, __name__ really is "__main__", since that’s the file you executed directly.

Organizing Multiple Modules into a Package

A single file is a module; a folder of related modules is a package. To turn a plain folder into a package, add a file named __init__.py inside it. Even an empty one is enough to make Python treat the folder as importable:

text
shapes/
โ”œโ”€โ”€ __init__.py
โ”œโ”€โ”€ circle.py
โ””โ”€โ”€ square.py
python
# shapes/circle.py
def area(radius):
    return 3.14159 * radius ** 2
python
# shapes/square.py
def area(side):
    return side ** 2

From outside the shapes folder, you can import from it just like any other module, using dot notation to reach inside:

python
from shapes import circle, square

circle.area(2)  # 12.56636
square.area(3)  # 9

__init__.py can also be used to control exactly what a package exposes when it’s imported. For instance, it can re-export specific functions so callers don’t need to know your internal file layout:

python
# shapes/__init__.py
from .circle import area as circle_area
from .square import area as square_area
python
from shapes import circle_area, square_area

circle_area(2)  # 12.56636

You’ve actually already used packages without necessarily noticing: beautifulsoup4 and requests, installed with pip3 back in Module 1, are both organized this exact way internally.

Try It

  1. Create a small module with two functions, then import and use them from a separate script.
  2. Add an if __name__ == "__main__": block to that module, and confirm it runs when you execute the module directly but not when you import it elsewhere.
  3. Try from your_module import * versus importing one specific function, and notice the difference in what ends up in your namespace.
  4. Turn a folder containing two related modules into a package with __init__.py, and import from both using dot notation.

Recap

  • A module is just a .py file whose functions (and other code) you can import elsewhere.
  • import module, import module as alias, from module import name, and from module import name as alias are the main import styles: prefer importing exactly what you need over import *.
  • if __name__ == "__main__": runs its block only when a file is executed directly, not when it’s imported.
  • A folder becomes an importable package once it contains an __init__.py file, which can also re-export specific names so callers don’t need to know your internal file layout.

Next lesson: a tour of some of Python’s most useful built-in modules.