LEARN PYTHON | Decomposition And Reusable Modules

Lesson objective

Break a program into cohesive functions and modules, import them safely, and separate reusable work from the application entry point.

Learn

04.3 | DECOMPOSITION AND REUSABLE MODULES

01 | START WITH RESPONSIBILITIES

A long program often mixes receiving input, checking it, calculating answers and displaying results. When all four jobs are tangled together, a small change can be difficult to isolate. Decomposition means breaking a problem into manageable responsibilities.

For a club ticket program, the jobs might be collect quantities, calculate ticket cost and display a report. These are useful boundaries because each has a different reason to change. Changing report wording should not require rewriting arithmetic.

A function is one tool for decomposition. A module groups related definitions in a Python file. A module is not a promise that the code is well organised: you still need sensible responsibilities and clear data flow.

02 | MAKE INPUTS AND OUTPUTS VISIBLE

Before writing a helper, describe its contract. ticket_total receives non-negative quantity and numeric unit price, returns their product and prints nothing. Its caller is responsible for collecting and validating input.

A function that receives data can be tested with direct calls. A function that reads hidden globals and asks for input is harder to reuse in a different interface. This is coupling: how strongly one part depends on another part’s details.

Aim for cohesion, meaning the contents of a part belong together. Several ticket pricing helpers fit naturally in pricing.py. A file combining ticket prices, unrelated game commands and printer settings has less coherent responsibility.

def ticket_total(quantity, unit_price):
    """Return cost for suitable non-negative numeric inputs."""
    return quantity * unit_price

print(ticket_total(3, 4.5))

EXPECTED OUTPUT

13.5

03 | PUT REUSABLE DEFINITIONS IN A FILE

Create pricing.py and put the function below in it. Save the file in the same folder as the application for this first example. Do not name it random.py, math.py or another standard-library module name: a local file with that name may shadow an intended import.

A module has its own global namespace. Its top-level definitions are not automatically inserted into every other file. Importing gives the caller an explicit way to access them.

Copy only the code into pricing.py, not the filename heading or the interactive shell prompt. Save both files before running the application. The module must be available through Python’s import search path; keeping this beginner pair together makes that straightforward.

def ticket_total(quantity, unit_price):
    """Return quantity multiplied by unit price."""
    return quantity * unit_price

04 | IMPORT AND USE A QUALIFIED NAME

In a separate app.py, import pricing binds the module under the name pricing. pricing.ticket_total accesses the function inside it. The dot makes the origin explicit and helps prevent conflicts with similarly named functions elsewhere.

from pricing import ticket_total instead binds that function directly in the importing module. Both approaches can work. Avoid from pricing import * in these projects because it makes the origin of names harder to follow.

Imports normally execute a module’s top-level code on the first import in a process and then use the cached module. Changing a saved file while an interactive process remains open may require restarting that process. Repeated import statements are not a reliable way to rerun edits.

# app.py, saved beside pricing.py
import pricing

cost = pricing.ticket_total(3, 4.5)
print(f"Ticket cost: {cost:.2f}")

EXPECTED OUTPUT

Ticket cost: 13.50

05 | KEEP IMPORTS FROM STARTING THE APPLICATION

A module may contain definitions and a runnable demonstration. If its top-level code immediately asks for input, simply importing it can unexpectedly pause or launch the program. Use a main function and the entry-point guard below.

Python sets __name__ to "__main__" when this file runs as the main script. When another file imports it normally, __name__ is its module name. The condition therefore calls main() for direct execution, while keeping the demonstration out of an ordinary import.

The guard does not prevent all top-level code from running on import. Imports, definitions, constant assignments and any unguarded print still execute. Place application-starting actions inside main or another explicitly called function.

# pricing.py
def ticket_total(quantity, unit_price):
    return quantity * unit_price

def main():
    print("Example cost:", ticket_total(3, 4.5))

if __name__ == "__main__":
    main()

06 | CHOOSE A SIMPLE DEPENDENCY DIRECTION

Imagine app.py imports pricing.py, but pricing.py imports app.py to access a variable. This circular dependency can expose partially initialised modules and makes it difficult to see which part owns the data.

Instead, pass the needed value as a parameter. The application can import helpers, and helpers can receive the data they need without importing the application back. Shared constants may belong in a small configuration module when that responsibility is clear.

A dependency is a connection, not automatically a fault. Your program needs connections. The goal is to make them understandable and avoid unnecessary reliance on implementation details.

07 | REFACTOR WITHOUT CHANGING THE REQUIREMENTS

Refactoring changes the organisation of code while preserving its intended behaviour. Moving an existing calculation into a function should not silently change its formula, units or accepted inputs.

Record known examples before the move. After the change, rerun them and compare outputs. When a formula changes at the same time as structure, it becomes harder to identify which change caused a mismatch.

Choose useful module boundaries, not a new file for every short statement. Two or three related modules are enough for the adventure project: world data, game actions and the command-line application.

08 | TRACE THE ENTRY-POINT GUARD

Choose direct execution or ordinary import. The model explains what top-level definitions and the guard do in each case. It describes the fixed module above; it is not a Python import environment.

    09 | TEST A MODULE WITHOUT ITS INTERFACE

    For a small arithmetic helper, direct assertions can check expected values. assert condition raises AssertionError if the condition is false. It is useful here for development checks; it is not application input validation, and Python can omit assertions when optimisations are enabled.

    Save this test as a separate file beside pricing.py and run it. Passing assertions are silent, so the final printed message confirms this simple script reached its end. More systematic unit testing comes later in the course.

    Tests should reflect the contract. A helper promising only suitable non-negative numeric inputs is not automatically responsible for accepting arbitrary text. Make new validation requirements explicit before adding them.

    import pricing
    
    assert pricing.ticket_total(3, 4.5) == 13.5
    assert pricing.ticket_total(0, 4.5) == 0
    assert pricing.ticket_total(2, 0) == 0
    print("Pricing checks passed")

    EXPECTED OUTPUT

    Pricing checks passed

    10 | HAND OVER A PROJECT SOMEONE CAN RUN

    A modular project includes all its required files and a clear starting command. Tell a partner to keep the files in one folder and run app.py, not whichever file happens to open first. Explain which module owns which responsibility.

    When an import fails, check the filename, current interpreter, saved location and spelling before changing unrelated code. A local file that shadows a library or an application importing itself can produce misleading symptoms.

    For the text adventure, you will keep world descriptions separate from action rules and the interactive loop. This will make it easier to change the story without copying navigation code into every room.

    PUT YOUR SKILLS TO WORK

    Use the Section 4 challenges to practise these ideas independently. Work in your Python editor, predict results and keep a short test record.

    Section 4 challenges Python course navigation

    MATCH THE PYTHON TERM TO ITS MEANING

    Terminology

    Terminology

    Decomposition

    Breaking a problem into manageable responsibilities.

    Module

    A Python file providing its own namespace and executable top-level code.

    Import

    Loading a module and binding a module or selected names.

    Qualified name

    A name such as pricing.ticket_total showing the containing module.

    Namespace

    A mapping between names and objects in a scope.

    Cohesion

    How closely the responsibilities within one component belong together.

    Coupling

    The degree of dependency between components.

    Dependency

    A connection where one component needs another.

    Circular import

    A dependency cycle involving modules importing each other.

    Entry point

    The location where the application starts its main work.

    __name__

    A module name attribute; __main__ for the main script.

    Main guard

    A condition allowing startup only when the file is the main script.

    Refactoring

    Restructuring code while preserving intended behaviour.

    Helper

    A function supporting a clearly defined smaller job.

    Import path

    Locations Python searches when resolving an import.

    Shadowing

    A local file or name hiding an intended module or binding.

    Assertion

    A development check raising AssertionError when false.

    Contract

    The stated inputs, outputs, assumptions and effects of a component.

    Questions

    Questions

    CHECK YOUR UNDERSTANDING

    Select all correct choices. Each exact set earns one point.

    1. What is decomposition?
    2. After import pricing, how is ticket_total accessed?
    3. Which code still executes on an ordinary first import?
    4. What does the main guard provide?
    5. What is refactoring?
    6. Which module design has better cohesion?
    7. Why can naming a local file random.py cause trouble?
    8. Which are sensible practices?

    EXPLAIN AND COMPARE

    Write your answers before revealing each sample.

    1. Decompose a ticket report into responsibilities.

    2. Explain cohesion using pricing.py.

    3. Explain import pricing and qualified access.

    4. Describe the main guard.

    5. Explain why the guard does not stop all import-time work.

    6. Suggest a way to remove a circular dependency.

    7. Explain refactoring and regression checks.

    8. Describe a useful modular-project handover.

    CODE IT | YOUR INDEPENDENT CHALLENGE

    Split a small report into a reusable helper module and an application with a main guard. Test direct execution and import.

    Work in your Python editor. Keep a working version, explain one design choice and record at least two tests. Use the Learn tab for worked examples before comparing your result.

    Flashcards

    Flashcards

    Click to flip. Select the ideas you need to revisit.

    0 cards selected for revision.

      Selections are kept while this page is open.

      Workbook

      Workbook

      COMING SOON

      The workbook for Decomposition And Reusable Modules is coming soon. Complete the matching task and written questions in your exercise book.