Python Modules and Self-Standing Scripts
Code that works once, in one notebook, is not yet a program. Turning it into one means dividing it along its responsibilities, putting those pieces in files other code can import, and giving the result a single, explicit place to start.
This post covers modules and imports, the namespaces they create, what separates a file
that is imported from one that is run, and the if __name__ == "__main__": guard that
lets the same file do both. It then applies all of it: a small project layout, a worked
conversion of notebook cells into a script, running that script from the terminal, and the
coding principles that keep a workflow reproducible.
Its companion post, Reading and Writing Files: Text, CSV, JSON, and JSONL, covers the file handling these programs are built around.
Modules and imports
A module is a Python file containing reusable code.
For example:
analysis_helpers.py
is a Python module named:
analysis_helpers
Modules support decomposition. Instead of putting every function into a single notebook or script, code can be divided into files with coherent responsibilities.
Import from the standard library
Python includes many standard-library modules.
import math
print(math.sqrt(25))
Output:
5.0
In this example:
mathis a module.sqrtis a name defined in that module.math.sqrtis the fully qualified name.
Another example:
from pathlib import Path
project_path = Path("data") / "students.csv"
print(project_path)
Namespaces
A namespace is a mapping between names and objects. It helps Python determine what a name refers to.
import math
result = math.sqrt(16)
The name sqrt belongs to the math namespace, so we write math.sqrt(16).
Namespaces prevent collisions. Different modules may define names such as mean, load, read, or process. The module prefix makes it clear which one is intended.
Common import styles
import math
Use:
math.sqrt(9)
from math import sqrt
Use:
sqrt(9)
import math as m
Use:
m.sqrt(9)
For beginner-friendly and shared code, explicit imports are often clearest:
import csv
import json
import math
from pathlib import Path
Avoid wildcard imports
Avoid:
from math import *
This imports many names into the current namespace. It becomes difficult to identify where functions came from and can create confusing name conflicts.
Local helper modules
Suppose a project is organized this way:
student-project/
├── data/
│ └── students.csv
├── output/
├── analysis_helpers.py
└── summarize_students.py
The helper module can define reusable analysis functions.
analysis_helpers.py:
from collections import Counter
def count_by_program(students):
return Counter(student["program"] for student in students)
def average_gpa(students):
gpas = [float(student["gpa"]) for student in students if student["gpa"]]
return sum(gpas) / len(gpas)
summarize_students.py:
from analysis_helpers import average_gpa, count_by_program
students = [
{
"student_id": "1001",
"name": "Amina Patel",
"program": "MS Data Science",
"gpa": "3.8",
},
{
"student_id": "1002",
"name": "Jordan Lee",
"program": "MS Analytics",
"gpa": "3.6",
},
]
print(count_by_program(students))
print(average_gpa(students))
Why helper modules matter
A project can separate responsibilities:
- One file manages inputs and outputs.
- One file contains cleaning or validation functions.
- One file contains analysis functions.
- One file runs the workflow.
- One file contains tests.
This structure is easier to review, test, reuse, and maintain than one large script.
Do not shadow standard-library modules
Avoid local filenames such as:
csv.py
json.py
math.py
pathlib.py
random.py
statistics.py
If a project includes csv.py, then this statement:
import csv
may import the local file instead of the intended standard-library module. This produces confusing errors.
Imported files versus executed scripts
A Python file can be used in two related ways:
- It can be run directly as a program.
- It can be imported by another Python file.
Consider greetings.py:
def greet(name):
return f"Hello, {name}!"
print("greetings.py is running")
Run directly:
python greetings.py
Output:
greetings.py is running
Now import it from another program:
import greetings
print(greetings.greet("Amina"))
Output:
greetings.py is running
Hello, Amina!
Why does it print the first message during import? Importing a module executes its top-level code. This can be undesirable when that code launches processing, writes files, or performs other work that should occur only when a program is intentionally executed.
The __main__ guard and program entry points
Python gives every module a special variable named __name__.
- When a file is run directly,
__name__is"__main__". - When a file is imported,
__name__is the module’s name.
The standard pattern is:
if __name__ == "__main__":
main()
Example:
def greet(name):
return f"Hello, {name}!"
def main():
print(greet("Amina"))
if __name__ == "__main__":
main()
When run directly:
python greetings.py
Output:
Hello, Amina!
When imported:
from greetings import greet
print(greet("Jordan"))
Output:
Hello, Jordan!
The imported module’s main() function does not automatically run.
Why an entry point matters
The main guard lets the same file be both:
- A reusable module that defines functions.
- A standalone executable script that coordinates a workflow.
This supports testing, reuse in notebooks, and use in larger pipelines without accidentally running a complete file-processing job upon import.
Recommended small-project structure
A simple introductory project can use this structure:
project-name/
├── README.md
├── data/
│ ├── raw/
│ │ └── source_data.csv
│ └── processed/
├── output/
│ ├── tables/
│ └── reports/
├── src/
│ ├── helpers.py
│ └── process_data.py
└── tests/
A smaller version is suitable for early assignments:
project-name/
├── data/
│ └── input.csv
├── output/
├── helpers.py
└── main.py
Folder and file responsibilities
| Folder or file | Purpose |
|---|---|
data/ | Input data and, when appropriate, processed data |
data/raw/ | Original source data that should generally not be changed |
data/processed/ | Cleaned, transformed, or derived datasets |
output/ | Generated reports, summary tables, charts, and results |
src/ | Source code for the project |
tests/ | Automated checks for important functions |
README.md | Setup, inputs, execution instructions, outputs, and assumptions |
main.py or process_data.py | Main executable workflow |
Why separation matters
Do not mix raw inputs and generated outputs in the same folder without a clear convention. Separation helps answer:
- Which files came from an external source?
- Which files were created by the analysis?
- Which files can be overwritten safely?
- Which code created a particular output?
- Which outputs should be shared with stakeholders?
This supports provenance, auditability, maintenance, and accidental-overwrite prevention.
Convert notebook work into a script
Notebooks are good for exploration. The goal is not to eliminate them. The goal is to distinguish between exploratory work and a repeatable operational workflow.
Typical notebook pattern
# Cell 1
import csv
# Cell 2
with open("students.csv") as file:
...
# Cell 3
# Clean data
# Cell 4
# Analyze data
# Cell 5
# Write result
Potential issues:
- Cells may run in a different order than they appear.
- Variables may remain in memory from earlier work.
- Paths and assumptions may be scattered across cells.
- The notebook may work only because of hidden state.
- A collaborator may not know how to reproduce the output from scratch.
Script-oriented design
Move work into named functions:
def read_data(input_path):
...
def clean_data(records):
...
def analyze_data(records):
...
def write_results(results, output_path):
...
def main():
input_path = ...
output_path = ...
records = read_data(input_path)
clean_records = clean_data(records)
results = analyze_data(clean_records)
write_results(results, output_path)
if __name__ == "__main__":
main()
This expresses a visible pipeline:
\[ \text{Read} \rightarrow \text{Clean} \rightarrow \text{Analyze} \rightarrow \text{Write} \]Benefits
- Each function has a specific responsibility.
- Functions can be tested independently.
- Inputs and outputs are explicit.
- The workflow can run from a terminal.
- The code is easier to review and maintain.
- A project can be handed off to another person.
- A notebook can still import and use the same functions for exploration.
Run scripts from the terminal
Suppose the project looks like this:
student-project/
├── data/
│ └── students.csv
├── output/
└── summarize_students.py
Open a terminal and change to the project directory:
cd path/to/student-project
Run the script:
python summarize_students.py
On some systems, Python 3 may be invoked as:
python3 summarize_students.py
Why terminal execution matters
Terminal execution begins the transition toward automation:
- A program can run without manually opening a notebook.
- The exact command can be documented in the README.
- A workflow can later be scheduled or integrated into CI/CD.
- The same script can run in a container, server, cluster job, or automated pipeline.
- Future sessions can add command-line arguments instead of fixed file paths.
At this stage, the central goal is simple: the program should run reliably from a clean starting state.
Coding principles for data-science projects
Beyond syntax, a few habits are what make a project maintainable.
Make inputs and outputs explicit
A reader should be able to identify:
- Input file locations.
- Output file locations.
- Expected input columns or JSON fields.
- Transformations performed.
- Summary statistics or deliverables produced.
Avoid hidden dependencies and unexplained state.
Use meaningful names
Prefer:
student_records
average_gpa
input_path
output_path
program_counts
over:
x
data2
temp
thing
result1
Good names reduce the effort required to read, review, manage, and maintain a program.
Keep functions focused
Prefer separate functions with coherent responsibilities:
def read_students(input_path):
...
def validate_students(students):
...
def calculate_average_gpa(students):
...
def write_summary(summary, output_path):
...
Avoid a single function that reads input, cleans data, analyzes it, writes results, and performs unrelated actions.
Preserve raw data
Treat source data as read-only whenever possible.
Instead of overwriting:
data/survey.csv
write a derived file:
data/processed/survey_cleaned.csv
This preserves provenance: the team can inspect the original input and understand what transformations created the derived data.
Validate early
Check assumptions near the beginning of a workflow:
- Does the input file exist?
- Is it empty?
- Does it have required CSV columns or JSON fields?
- Are identifiers unique when they should be?
- Are numeric fields actually numeric?
- Are dates parseable?
- Are values within plausible ranges?
- Are required fields missing?
Early validation prevents bad data from silently creating misleading results.
Report what happened
A script should summarize its work.
Input file: data/input.csv
Rows read: 850
Rows skipped: 12
Rows analyzed: 838
Output file: output/category_summary.csv
Categories found: 6
This supports quality assurance, troubleshooting, operational transparency, and stakeholder communication.
Project-management checklist
A project manager need not write every Python function, but should be able to determine whether a workflow is sufficiently specified, reproducible, and trustworthy.
Inputs
- What person, system, or process provides the input data?
- What file format is expected: text, CSV, JSON, JSONL, or another format?
- Which CSV columns or JSON fields are required?
- What do missing values mean?
- Which date, time-zone, unit, category, and identifier conventions apply?
- How often is the input refreshed?
- Does the process preserve the original data?
Processing
- Which transformations occur?
- Are those transformations documented?
- How does the workflow handle invalid or incomplete records?
- Does it report input, skipped, and output record counts?
- Can it be rerun from scratch?
- Are intermediate datasets retained when auditability requires them?
Outputs
- What reports, tables, files, or derived datasets are produced?
- What fields and definitions do they contain?
- Who uses the output?
- How is output quality verified?
- Where are outputs stored?
- Are outputs dated, versioned, or otherwise traceable?
Operational readiness
- Can another person run the project from documented instructions?
- Does it use portable project-relative paths instead of personal desktop paths?
- Does it provide understandable errors?
- Are dependencies and versions documented?
- Is there a clear entry point such as
python src/process_data.py?
Key takeaways
- Modules support code reuse and separation of responsibilities.
- Imports create namespaces; use clear imports and avoid ambiguous local module names.
if __name__ == "__main__":distinguishes reusable code from code that should run as a program.- A script needs one explicit entry point, so the same file can be imported without side effects or run directly from the terminal.
- Keep functions focused, name things meaningfully, validate early, and report what happened.
- A maintainable data-science project makes its inputs, transformations, outputs, assumptions, and execution steps visible.