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_price04 | 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