Taming Python: Modularization

Taming Pythons
Who’s strangling who?

For exercise 2, I want to add modularization so the program can be expanded by having classes added, and no changes need to be done in the main code.

✅ Step-by-step plan

  1. Create a dynamic list of modules using the python project’s file structure
  2. Display a list of modules (program options)
  3. Take user input to determine which module to run
  4. Return to the menu screen after the module is exited

Begin with some library imports…

import pkgutil    # tools for working with modules
import importlib  # access tools for more robust import use
import inspect    # debugging tools

from PyProgramBase import ProgramBase # importing my own class

For this we will use the project file structure to determine which modules are available.

PyHelloWorld/
│
├── main.py
├── PyProgramBase.py
└── programs/
    ├── init.py
    ├── PyHelloWorld.py
    ├── PyProgram2.py
    └── PyProgram3.py

I will scan the programs folder from main.py to discover all available programs.
I decided to go with a structured naming scheme in case any other files are needed in the folder which I might not be a python module.

def discover_programs():
    """Auto-load all ProgramBase subclasses from modules starting with 'Py'."""
    programs = []
    package = "programs"

    # iterate through modules inside 'programs'
    for loader, module_name, _ in pkgutil.iter_modules([package]):
        # only load modules beginning with "Py"
        if not module_name.startswith("Py"):
            continue

        module = importlib.import_module(f"{package}.{module_name}")

        # inspect module for subclasses of ProgramBase
        for _, obj in inspect.getmembers(module, inspect.isclass):
            if issubclass(obj, ProgramBase) and obj is not ProgramBase:
                programs.append(obj)

    return programs

Building the program list from the results of discover_programs and then displaying the list for the UI.

def build_menu(program_classes):
    """Map numbers → (label, class)."""
    menu = {}
    for i, cls in enumerate(program_classes, start=1):
        menu[i] = (cls.__name__, cls)
    return menu

def show_menu(menu):
    print("""
========================
        MAIN MENU
========================
""")
    for number, (label, _) in menu.items():
        print(f"{number}. {label}")
    print("0. Exit\n")

I may return to this to add a menu name for each module, as this shows the class name for each item.

Lastly, this is the result for main()

def main():
    program_classes = discover_programs()
    menu = build_menu(program_classes)

    while True:
        show_menu(menu)
        choice = input("Enter option: ").strip()

        if choice == "0":
            print("Goodbye!")
            break

        if not choice.isdigit() or int(choice) not in menu:
            print("Invalid selection.\n")
            continue

        _, program_cls = menu[int(choice)]
        instance = program_cls()
        instance.run()

        print("\n--- Finished ---\n")

The original file will be renamed main.py as that is the entry point for the whole app, and then PyProgramBase will be the library for loading the individual modules. This very simple class base is needed for discovery and modularity, it seems. As a solo project, this could be skipped, but I’m trying to learn this for the sake of being able to use it in a professional environment, so this seems to be good practice.

class ProgramBase:
    """Base class all programs must extend."""
    def run(self):
        raise NotImplementedError("Subclasses must implement run().")

Most of the code from the original HelloWorld file gets moved to a module. Since this is now a module and not the whole program, I don’t need the if statement here…

# programs/PyHelloWorld.py

from PyProgramBase import ProgramBase

class HelloWorldProgram(ProgramBase):
    def run(self):
        print("Hello World... anticlimactic, right?")

This was a lot, and I had it completed before I created this blog post, so it doesn’t get into the roadblocks I had run into at the time. The modules start easy but escalate quickly once there is user interaction to account for. Future posts in this python series should get more into the weeds with problem-solving.

By:


Leave a comment