Creating a Module in Python: Complete Custom Module Development and Import System Implementation Guide

Creating a module in python

Creating a module in python

When I first started writing Python scripts, I kept everything in one file. It worked fine until my “one file” ballooned into eight hundred lines of tangled functions, and I couldn’t find anything anymore. That’s the moment I actually sat down and learned how modules work in Python — not just how to import one, but how to build my own. In this guide, I’m going to walk you through everything I’ve learned about creating modules, from the absolute basics to the internal mechanics of how Python’s import system actually works under the hood.

What Exactly Is a Module?

In Python, a module is simply a file containing Python code — variables, functions, classes, or runnable statements — that ends with a .py extension. That’s it. There’s no special syntax needed to “declare” a file as a module. The moment you save a file as mymodule.py, it becomes an importable module.

I like to think of modules as the building blocks of Pythonic organization. Instead of writing everything in one script, I split logic into files based on responsibility: one file for database helpers, one for string utilities, one for configuration. This is the same principle behind Python’s own standard library, which is really just a huge collection of modules.

Creating My First Custom Module

Let’s say I want to build a small module with some math utility functions. I’ll create a file called mathutils.py:

# mathutils.py

def add(a, b):
    """Return the sum of two numbers."""
    return a + b

def subtract(a, b):
    """Return the difference of two numbers."""
    return a - b

def is_even(n):
    """Check whether a number is even."""
    return n % 2 == 0

PI = 3.14159

That’s a complete, working module. There’s no boilerplate, no class wrapper required, no special header. Now, in another file placed in the same directory, I can import it:

# main.py
import mathutils

print(mathutils.add(4, 5))       # Output: 9
print(mathutils.subtract(10, 3)) # Output: 7
print(mathutils.is_even(8))      # Output: True
print(mathutils.PI)              # Output: 3.14159

Output:

9
7
True
3.14159

Notice that I access everything through mathutils. — that’s the module’s namespace. This is one of Python’s most underrated design decisions: modules act as namespaces, which prevents naming collisions between different parts of a large codebase.

Different Ways to Import

I don’t always import a whole module. Depending on the situation, I use one of these patterns:

import mathutils                      # import the whole module
from mathutils import add, subtract   # import specific names
from mathutils import *               # import everything (I avoid this in real projects)
import mathutils as mu                # import with an alias

The from mathutils import * pattern looks convenient, but I’ve learned to avoid it in production code because it pollutes the local namespace and makes it hard to tell where a function actually came from. If mathutils and another module both define add, you’ll get silent bugs.

How Python Actually Finds Your Module (The Import System Internals)

This is the part most tutorials skip, and it’s the part that actually matters when things go wrong. When I write import mathutils, Python does the following, in order:

  1. Checks sys.modules — a cache dictionary of already-imported modules. If mathutils was already imported somewhere in the program, Python just reuses that cached module object instead of re-reading the file. This is why module-level code only runs once no matter how many times you import it.
  2. Searches sys.path — a list of directories Python checks, in order, to locate the module. This list includes the directory of the script being run, the PYTHONPATH environment variable (if set), and the standard library’s installation directories.
  3. Loads and compiles the file — once found, Python compiles the .py file into bytecode and executes it top to bottom, populating a new module object with everything defined in it.
  4. Caches it — the resulting module object is stored in sys.modules for future imports.

I can inspect this myself:

import sys
print(sys.path)

Output (abbreviated, will vary per machine):

['', '/usr/lib/python3.11', '/usr/lib/python3.11/lib-dynload', '/usr/local/lib/python3.11/site-packages']

If my module isn’t in one of these directories, Python raises ModuleNotFoundError. This is the single most common issue I see beginners run into, and now you know exactly why it happens.

The __pycache__ Folder and Compiled Bytecode

After I import a module for the first time, I usually notice a __pycache__ folder appear next to it, containing a file like mathutils.cpython-311.pyc. This is Python caching the compiled bytecode so it doesn’t have to re-parse the source code every time the module is imported in a future run. Python compares file modification timestamps to decide whether to use the cached bytecode or recompile. This is a small but real performance optimization, especially for large modules that get imported frequently.

The if __name__ == "__main__": Pattern

Every module has a built-in variable called __name__. When a file is run directly, __name__ is set to "__main__". When the same file is imported as a module elsewhere, __name__ is set to the module’s filename instead. I use this constantly to let a file behave both as a standalone script and as an importable module:

# mathutils.py

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

if __name__ == "__main__":
    # This block only runs when I execute this file directly
    print("Testing mathutils module...")
    print(add(2, 3))

If I run python mathutils.py directly, I get:

Testing mathutils module...
5

But if I import it from another file, that block never executes — only the function definitions get loaded. This pattern is everywhere in real-world Python code, and once I understood it, a lot of open-source code suddenly made much more sense.

Organizing Modules into Packages

Once I had more than a handful of related modules, I grouped them into a package — a directory containing an __init__.py file (in modern Python, this file can even be empty or omitted entirely for a namespace package, but I still include it for clarity):

myproject/
├── main.py
└── utils/
    ├── __init__.py
    ├── mathutils.py
    └── stringutils.py

From main.py, I can now import like this:

from utils import mathutils
from utils.stringutils import capitalize_words

The __init__.py file runs automatically the moment the package is imported, so I often use it to expose a clean public API:

# utils/__init__.py
from .mathutils import add, subtract

This lets someone do from utils import add directly, without needing to know it actually lives in mathutils.py.

Performance and Memory Considerations

Module-level code executes exactly once per process, and the resulting module object stays in memory for the lifetime of the program via sys.modules. This has a few practical implications I keep in mind:

Common Mistakes I’ve Made (and Fixed)

Real-World and Professional Use Cases

In actual projects I’ve worked on, module design directly affects maintainability. A typical professional Flask or Django project splits code into modules like models.py, views.py, utils.py, and config.py. Data science projects often separate preprocessing.py, features.py, and evaluation.py. Automation scripts I write for personal use — scraping, file renaming, report generation — each get their own reusable module so I can import shared logic (like a logging setup or an email sender) across multiple scripts without duplicating code.

Frequently Asked Questions

Do I need to install anything to create a module? No. Any .py file is automatically a module. No installation or registration step is required.

What’s the difference between a module and a package? A module is a single .py file. A package is a directory of modules (optionally containing an __init__.py) that lets you organize related modules together.

Can I import a module from a different directory? Yes, but the directory needs to be on sys.path. I usually do this by adding the directory dynamically with sys.path.append(), using a proper package structure, or installing the project with pip install -e . for larger projects.

Why does my module only run once even though I import it multiple times? Because Python caches modules in sys.modules after the first import, so repeated imports just reuse the cached object instead of re-executing the file.

Summary

Creating a module in Python is as simple as saving a .py file, but understanding what happens underneath — the sys.path search, the sys.modules cache, bytecode compilation, and the __name__ mechanism — is what separates writing scripts from building maintainable software. Once I started organizing my code into small, focused modules and packages, my projects became dramatically easier to test, reuse, and reason about.

References

Exit mobile version