Python Programming

Chapter 6
Modules, Packages and Reusable Program Design

Diploma in Data science
Python Programming   •   Diploma

Modules, Packages and Reusable Program Design

Introduction

A small Python program can often be written in one file. As a program grows, however, placing every function, class, constant, and instruction in one file makes the system difficult to understand, test, maintain, and reuse. Professional software is therefore organized into modules and packages. These structures divide a program into coherent components with clear responsibilities and interfaces.

Python provides a powerful module system, a large standard library, and packaging tools that support both small scripts and large applications. Effective organization also requires an understanding of namespaces, import behavior, dependency management, circular dependencies, and the distinction between a component’s public interface and its private implementation.

This chapter develops these ideas through practical examples relevant to data science and general software development.

Learning Objectives

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

  • explain the purpose of modules, packages, and namespaces;
  • create and import user-defined modules;
  • distinguish common forms of Python imports and choose between them appropriately;
  • use selected standard-library modules;
  • design packages with clear public interfaces;
  • manage third-party dependencies using virtual environments and requirement files;
  • identify and reduce problems such as circular imports and namespace pollution;
  • apply principles of reusable, testable, and maintainable program design.

1. Why Modular Design Matters

A module is a Python file containing definitions and executable statements. A module normally has a focused purpose, such as reading data, validating records, or calculating descriptive statistics. A package is a structured collection of related modules.

Modular design provides several benefits:

  1. Separation of concerns: each component performs a related group of tasks.
  2. Reusability: a function written for one program can be imported into another.
  3. Maintainability: changes can be localized to a small part of the system.
  4. Testability: individual functions or modules can be tested independently.
  5. Collaboration: different developers can work on separate components.
  6. Dependency control: required libraries and versions can be documented.

A useful design principle is that a module should have high cohesion and low coupling. High cohesion means that its contents belong together. Low coupling means that it depends on as little unrelated code as possible.

For example, a data-processing application might be organized as follows:

sales_app/
├── main.py
├── io_utils.py
├── validation.py
├── statistics.py
└── reports.py

Here, io_utils.py might read CSV files, validation.py might check records, and reports.py might format results. The main program coordinates these components instead of implementing all details itself.

2. Modules and Namespaces

When Python loads a module, it creates a namespace: a mapping between names and objects. Functions, classes, variables, and imported names are stored in this mapping.

Consider the following module:

# geometry.py
PI = 3.141592653589793

def area_of_circle(radius):
    if radius < 0:
        raise ValueError("radius cannot be negative")
    return PI * radius ** 2

Another file can import it:

import geometry

area = geometry.area_of_circle(3)
print(area)

The expression geometry.area_of_circle explicitly identifies the function in the geometry namespace. This qualified form improves readability and prevents accidental name collisions.

Python has several relevant namespace levels:

  • Built-in namespace: names such as len, print, and ValueError.
  • Global namespace: names defined at module level.
  • Enclosing-function namespace: names in an outer function when nested functions are used.
  • Local namespace: names created inside the current function.

Python resolves names according to the LEGB rule: Local, Enclosing, Global, Built-in. This rule is important when a local variable has the same name as a module-level or built-in name.

len = 10

values = [4, 7, 2]
# len(values) now raises TypeError because the built-in name was shadowed

Using names such as list, str, or sum for variables is therefore poor practice. If a built-in has been shadowed, the safest solution is usually to rename the variable.

3. Importing Modules

Python supports several import styles.

3.1 Importing a module

import math

root = math.sqrt(81)

This is generally the clearest style because the origin of sqrt is visible.

3.2 Importing with an alias

import statistics as stats

mean_value = stats.mean([10, 12, 15])

Aliases are useful when a module has a long name or when a conventional abbreviation is widely understood. They should not be used to hide the meaning of a dependency.

3.3 Importing selected names

from pathlib import Path

file_path = Path("data") / "sales.csv"

This is appropriate when a few names are central to the program. It places the imported name directly into the current namespace.

3.4 Avoiding unrestricted wildcard imports

from math import *

Wildcard imports make it unclear where names came from and may overwrite existing names. They are normally unsuitable for production programs.

3.5 Import execution and caching

When a module is imported, Python executes its top-level statements once during that interpreter session. The loaded module is then kept in sys.modules. Later imports normally reuse the cached module rather than executing the file again.

This explains why top-level code should be limited. A module should define functions, classes, and constants; it should not unexpectedly delete files, send messages, or start a long computation merely because another module imports it.

4. The __name__ Convention

A module can be used in two ways: imported by another module or executed directly. Python sets the special variable __name__ to the module’s import name. When the file is run directly, its value is '__main__'.

# converter.py

def kilometres_to_miles(kilometres):
    return kilometres * 0.621371


def main():
    print(kilometres_to_miles(10))


if __name__ == "__main__":
    main()

The conditional block is called a main guard. It allows the file to be executed as a program without running main() when the file is imported.

This pattern is especially useful for demonstration code, command-line entry points, and simple module-level tests.

5. Designing a Reusable Module

A reusable module should have a focused purpose, predictable behavior, meaningful documentation, and minimal hidden state. Its functions should usually receive inputs as arguments and return outputs rather than relying on global variables.

Poor design:

# poor_calculator.py
total = 0

def add(value):
    global total
    total += value

This function changes hidden global state. Its result depends on what happened previously, which complicates testing and reuse.

Improved design:

# calculator.py
def add(first, second):
    return first + second

The improved function is deterministic for the same inputs and has a clear interface.

A function’s interface includes:

  • its name;
  • parameters and expected types or formats;
  • return value;
  • possible exceptions;
  • side effects;
  • assumptions and limitations.

Type hints can communicate intended use, although Python does not enforce them at runtime by default.

def average(values: list[float]) -> float:
    if not values:
        raise ValueError("values must not be empty")
    return sum(values) / len(values)

The function assumes that values contains numeric values and that an average of an empty collection is not meaningful. Explicitly handling that condition is better than allowing an obscure division error.

6. Packages

A package groups related modules under a common namespace. A typical package may have this structure:

analytics/
├── __init__.py
├── cleaning.py
├── summary.py
└── validation.py

The __init__.py file traditionally identifies a directory as a package and may contain package initialization code. Modern Python also supports namespace packages without this file in certain situations, but including a simple __init__.py remains common and clear for teaching and ordinary applications.

Modules can be imported using dotted names:

from analytics.cleaning import remove_blank_rows
from analytics.summary import mean_and_median

A package’s __init__.py can expose selected public objects:

# analytics/__init__.py
from .summary import mean_and_median

__all__ = ["mean_and_median"]

Now users may write:

from analytics import mean_and_median

The leading dot in .summary means that the import is relative to the current package. Relative imports can make internal package relationships explicit, while absolute imports are often easier to understand in larger applications. A project should adopt a consistent style.

The __all__ variable is a convention describing names intended for wildcard import and documenting part of a package’s public surface. It does not provide security or prevent direct access to other names.

7. Standard-Library Modules for Data-Science Work

Python’s standard library supplies reliable tools without requiring third-party installation.

pathlib: file and directory paths

from pathlib import Path

input_file = Path("data") / "observations.csv"
if input_file.exists():
    text = input_file.read_text(encoding="utf-8")

Path is preferable to manually joining strings because it handles platform-specific path conventions.

csv: tabular text files

import csv

with open("scores.csv", newline="", encoding="utf-8") as file:
    reader = csv.DictReader(file)
    rows = list(reader)

The with statement ensures that the file is closed when the block ends.

json: structured data interchange

import json

with open("settings.json", encoding="utf-8") as file:
    settings = json.load(file)

JSON represents common data structures, but it does not represent every Python object directly. Dates, sets, and custom classes need an explicit conversion strategy.

datetime: dates and times

from datetime import datetime, timezone

created_at = datetime.now(timezone.utc)

Using timezone-aware values is safer for systems operating across regions. A timestamp without timezone information may be ambiguous.

collections: specialized containers

from collections import Counter

counts = Counter(["red", "blue", "red"])
print(counts["red"])  # 2

statistics: basic descriptive statistics

from statistics import mean, median

values = [4, 8, 10, 12]
print(mean(values))
print(median(values))

For large-scale numerical analysis, libraries such as NumPy and pandas are commonly used, but they are third-party dependencies and must be managed explicitly.

8. Dependency Management

A dependency is software that a program requires. Dependencies may be part of the standard library or installed separately. A third-party dependency should not be assumed to exist merely because it is installed on the developer’s computer.

A virtual environment creates an isolated Python installation for a project. A typical workflow is:

python -m venv .venv

Activation commands depend on the operating system. After activation, packages can be installed with pip:

python -m pip install pandas

A project can record dependencies in a requirements file:

pandas>=2.0,<3.0
pytest>=8.0,<9.0

Install them with:

python -m pip install -r requirements.txt

Version constraints balance reproducibility and flexibility. An exact version can make an environment highly repeatable, while a bounded range permits compatible updates. For professional systems, dependency lock files and automated builds provide stronger reproducibility than an informal list alone.

Good dependency practice includes:

  • use a separate virtual environment for each project;
  • install packages through the project’s documented process;
  • record direct dependencies, not every transitive dependency manually;
  • update dependencies deliberately;
  • review security advisories and licensing conditions;
  • avoid installing packages from untrusted sources;
  • test the program after dependency upgrades.

Dependency management is also a governance concern. A data-science project may process personal or confidential information, so package selection should consider security, licensing, maintenance activity, and data-handling implications.

9. Circular Imports and Other Import Problems

A circular import occurs when module A imports module B while module B imports module A. During loading, one module may be only partially initialized, causing confusing ImportError or AttributeError messages.

For example:

orders.py imports customers.py
customers.py imports orders.py

Circular imports often indicate that responsibilities are poorly separated. Possible solutions include:

  1. move shared definitions into a third module;
  2. redesign the dependency direction;
  3. pass objects as function arguments instead of importing them globally;
  4. use a local import only when there is a well-understood reason.

Local imports can sometimes break an import cycle, but they should not be used merely to conceal poor architecture.

Another common problem is a filename that shadows a standard-library or third-party module. A file named statistics.py, for example, may prevent Python from importing the intended standard-library module. Project files should use distinctive names and avoid names such as random.py, json.py, and typing.py.

10. Public Interfaces and Encapsulation

Python does not enforce private module members in the same way as some languages. A leading underscore is a convention indicating that a name is internal:

_DEFAULT_LIMIT = 100

def _normalise_label(label):
    return label.strip().lower()

Users should normally depend on documented public functions rather than internal helpers. This creates an abstraction boundary: implementation details can change while the public interface remains stable.

A good public interface is:

  • small enough to understand;
  • consistent in naming and behavior;
  • documented with examples and exceptions;
  • independent of unnecessary implementation details;
  • stable across minor internal improvements.

Docstrings provide documentation close to the code:

def find_highest(values: list[float]) -> float:
    """Return the largest value in a non-empty sequence.

    Raises:
        ValueError: if values is empty.
    """
    if not values:
        raise ValueError("values must not be empty")
    return max(values)

11. Testing Reusable Components

Modular design supports unit testing. A unit test checks a small component independently from external systems.

# test_calculator.py
from calculator import add


def test_adds_two_numbers():
    assert add(2, 3) == 5

Useful tests include normal cases, boundary cases, invalid inputs, and expected exceptions. A function that reads files or accesses a database should be separated from the pure transformation logic where possible. This allows most logic to be tested quickly without requiring external resources.

A reusable component should also be evaluated for compatibility. Changing a function from calculate_total(items) to calculate_total(items, tax_rate) may break existing users. When an interface must change, the change should be documented and, where practical, introduced through a backward-compatible transition.

12. Worked Application: A Small Grade-Analysis Package

The following illustrative design separates concerns in a student-grade application.

gradebook/
├── __init__.py
├── validation.py
├── analysis.py
└── report.py
main.py

validation.py:

def validate_scores(scores):
    if not scores:
        raise ValueError("at least one score is required")
    if any(not isinstance(score, (int, float)) for score in scores):
        raise TypeError("scores must be numeric")
    if any(score < 0 or score > 100 for score in scores):
        raise ValueError("scores must be between 0 and 100")

analysis.py:

from statistics import mean
from .validation import validate_scores


def analyse(scores):
    validate_scores(scores)
    return {
        "count": len(scores),
        "mean": mean(scores),
        "highest": max(scores),
        "lowest": min(scores),
    }

report.py:

def format_analysis(result):
    return (
        f"Count: {result['count']}\n"
        f"Mean: {result['mean']:.2f}\n"
        f"Highest: {result['highest']}\n"
        f"Lowest: {result['lowest']}"
    )

main.py:

from gradebook.analysis import analyse
from gradebook.report import format_analysis

scores = [72, 84, 91, 65]
result = analyse(scores)
print(format_analysis(result))

The mean is calculated as the sum divided by the number of observations:

x̄ = (x₁ + x₂ + ⋯ + xₙ) / n

Here, xᵢ is the i-th score and n is the number of scores. For the four scores shown, the sum is 312 and n = 4, so the mean is 78. The formula applies when the values are numeric and the collection is non-empty. The package validates these conditions before analysis.

Case analysis

Background: A training department receives assessment scores from several classes.

Problem: An initial script combines file reading, validation, calculations, and report formatting in one 300-line file. Small changes frequently introduce errors.

Approach: The team creates separate modules for validation, analysis, and reporting. The analysis module depends on validation, but reporting depends only on the analysis result. A main guard is used for command-line execution, and unit tests cover invalid scores and summary calculations.

Outcome: Each component can be tested independently. A later change from text reports to JSON output affects the reporting layer without changing validation or statistical logic.

Learning points: The design demonstrates separation of concerns, a directional dependency structure, explicit interfaces, and reusable functions with limited side effects.

13. Best Practices and Limitations

The following practices are broadly useful:

  • give each module one coherent responsibility;
  • keep import statements near the top of the file;
  • prefer explicit imports over wildcard imports;
  • avoid executable side effects at import time;
  • use a main guard for script entry points;
  • document public functions and exceptions;
  • validate inputs at clearly defined boundaries;
  • avoid mutable global state;
  • use virtual environments and documented dependency versions;
  • test modules independently and test their integration together;
  • design dependencies in one direction where possible.

Modularity is not automatically beneficial. Excessive fragmentation can create many tiny files, complicated navigation, and unnecessary abstraction. A module boundary should represent a meaningful responsibility rather than a fixed number of lines. Similarly, a package should not hide a poorly designed interface behind a complex directory structure.

14. Contemporary Development Considerations

Modern Python projects commonly use automated formatting, static type checking, linting, unit testing, and continuous integration. Tools such as formatters and linters improve consistency; type checkers can detect certain interface errors before execution; and continuous integration can test a project across supported Python versions.

In data science, notebooks are valuable for exploration but can become difficult to reuse when code, outputs, and explanations are mixed together. Moving stable functions into modules makes experiments reproducible and enables automated testing. AI-assisted code-generation tools can help create module templates or tests, but generated code must be reviewed for correctness, security, licensing, hidden dependencies, and inappropriate data handling. Responsibility for the final design remains with the developer.

Chapter Summary

Modules divide Python programs into coherent files, while packages organize related modules under a common namespace. Imports make reusable definitions available, and qualified names help prevent collisions. The __name__ == "__main__" pattern separates importable definitions from direct execution.

Reusable design depends on focused responsibilities, explicit interfaces, limited side effects, meaningful documentation, and testable functions. Standard-library modules such as pathlib, csv, json, datetime, collections, and statistics provide important capabilities without external installation. Third-party dependencies should be isolated in virtual environments, documented, updated carefully, and evaluated for security and licensing.

Circular imports, namespace pollution, shadowed module names, and excessive global state reduce reliability. A well-designed package has clear public components, controlled dependencies, and an architecture that permits one part to change without destabilizing the whole application.

Key Terms

Module: A Python file containing definitions and executable statements.

Package: A structured collection of related Python modules.

Namespace: A mapping from names to objects.

Import: An operation that makes a module or selected definitions available.

Alias: An alternative name assigned during import.

Wildcard import: An import that attempts to bring many names into the current namespace.

Main guard: The if __name__ == "__main__": condition used to control direct execution.

Public interface: The documented functionality intended for users of a component.

Encapsulation: The practice of hiding implementation details behind an interface.

Dependency: Software required by a program.

Virtual environment: An isolated environment for project-specific Python packages.

Namespace package: A package assembled from directories that may not contain __init__.py.

Circular import: A dependency cycle in which modules import one another.

Cohesion: The degree to which elements of a module belong together.

Coupling: The degree of dependency between components.

Docstring: A string used to document a module, class, or function.

Standard library: The collection of modules distributed with Python.

Type hint: Annotation communicating an intended type or structure.

Side effect: A change to external or shared state caused by an operation.

Review Questions

Short Answer Questions

  1. What is the difference between a module and a package?
  2. Why is import module often clearer than a wildcard import?
  3. What is the purpose of the main guard?
  4. Why are virtual environments useful?
  5. What problem can circular imports cause?

Long Answer Questions

  1. Explain how namespaces and the LEGB rule influence name resolution in Python.
  2. Describe the characteristics of a well-designed reusable module.
  3. Compare standard-library dependencies with third-party dependencies and explain how each should be managed.
  4. Explain how a package can provide a stable public interface while hiding implementation details.
  5. Discuss how separation of concerns improves testing and maintenance in a data-science application.

Analytical / Application Questions

  1. A project contains data.py, analysis.py, and report.py. analysis.py imports report.py, while report.py imports analysis.py. Diagnose the design problem and propose a better dependency structure.
  2. Design a package for a small inventory application. Identify at least three modules, their responsibilities, and the public functions each should expose.
  3. A script works on its author’s computer but fails on a colleague’s computer because pandas is missing and a local file is named json.py. Explain the causes and give a systematic solution.

Multiple Choice Questions with Answers

Question 1

Which statement best describes a Python module?

A. A directory that must contain several programs

B. A Python file containing definitions and executable statements

C. A virtual environment used to install packages

D. A command-line argument passed to Python

Correct Answer: B. A Python file containing definitions and executable statements

Question 2

What is the main advantage of writing import statistics and then using statistics.mean(...)?

A. It prevents the module from being loaded

B. It automatically installs the module

C. It makes the origin of the name explicit

D. It converts the result to an integer

Correct Answer: C. It makes the origin of the name explicit

Question 3

When is main() called in a file containing if __name__ == "__main__": main()?

A. Whenever the file is imported

B. Only when the file is executed directly

C. Only when another module defines main()

D. Before Python creates the module namespace

Correct Answer: B. Only when the file is executed directly

Question 4

Which standard-library module is specifically designed for platform-independent file paths?

A. pathlib

B. statistics

C. collections

D. datetime

Correct Answer: A. pathlib

Question 5

What is a likely consequence of a wildcard import?

A. It creates a virtual environment

B. It prevents all name collisions

C. It can make the origin of names unclear

D. It automatically checks type hints

Correct Answer: C. It can make the origin of names unclear

Question 6

What is the primary purpose of a virtual environment?

A. To isolate project-specific packages

B. To convert Python code into machine code

C. To replace the standard library

D. To remove all package dependencies

Correct Answer: A. To isolate project-specific packages

Question 7

Which design most directly reduces hidden state and improves testability?

A. Using mutable global variables for results

B. Making functions depend on previous calls

C. Passing inputs as arguments and returning outputs

D. Putting all code in __init__.py

Correct Answer: C. Passing inputs as arguments and returning outputs

Question 8

A circular import occurs when:

A. A package contains two functions with the same name

B. Two modules depend on each other during loading

C. A program imports a standard-library module

D. A module is imported only once

Correct Answer: B. Two modules depend on each other during loading

Question 9

For scores [72, 84, 91, 65], what is the arithmetic mean?

A. 76

B. 77

C. 78

D. 79

Correct Answer: C. 78

Question 10

Which practice best supports a stable public interface?

A. Requiring users to call undocumented helper functions

B. Exposing focused, documented functions and hiding internal details

C. Using wildcard imports throughout the package

D. Changing parameter meanings without documentation

Correct Answer: B. Exposing focused, documented functions and hiding internal details

Python Programming
Chapter 6 of 15