Skip to content

Writing your own module

Goal: put the functions you keep reusing into a file of your own and import it, so that every new program starts with your toolkit ready.

Why this matters

Four times in this course you have been told to keep this function: ask_for_number() in Modules 5 and 7, read_lines() in Module 6, ask_for_float() and ask_for_date() in the exercises since. Keeping them has meant copying and pasting, and you probably have several slightly different versions by now. There is a better way, and you already know most of it. A module is just a file of Python code. random.py is one. Yours can be too.

A module is just a file

Here is a file with one function in it, and a print at the top for reasons that will become clear:

greetings.py
print(f"greetings.py is running, and __name__ is {__name__}")

def say_hello(name):
    """Return a friendly greeting for the given name."""
    return f"Hello, {name}!"

Run it directly and you get:

greetings.py is running, and __name__ is __main__

Now here is a second file, saved in the same folder, that imports the first:

use_greetings.py
import greetings

print(greetings.say_hello("Ada"))
print(greetings.say_hello("Grace"))
greetings.py is running, and __name__ is greetings
Hello, Ada!
Hello, Grace!

That is the whole trick. import greetings makes Python look for a file called greetings.py, starting in the folder of the program that is running. It finds yours, runs it, and everything defined in it is available as greetings.something, exactly as with math or random. Two things to take from the output:

  • Importing a file runs it. The print at the top of greetings.py appeared, before either greeting. Running the file is how Python gets the def lines to happen, so that the functions exist. It means a module's top level should contain definitions, and not much else. A module that asks the user questions or prints things at the top level will do so every time anyone imports it.
  • __name__ tells a file how it is being used. It is a variable Python sets in every file. When a file is run directly, __name__ is "__main__". When it is imported, __name__ is the module's name. That difference is useful, and the next section uses it.

You may also notice a new folder called __pycache__ appear next to your files. Python keeps a pre-digested copy of imported modules there so that the next import is faster. It is safe to ignore and safe to delete.

Your toolkit: helpers.py

Now the real thing. Every reusable function from the course, in one file:

helpers.py
"""Small functions for asking the user for input and reading files.

Import this from any program saved in the same folder:
    from helpers import ask_for_number
"""

from datetime import date

def ask_for_number(prompt):
    """Keep asking until the user types a whole number, then return it."""
    while True:
        text = input(prompt)
        try:
            return int(text)
        except ValueError:
            print("Please type a whole number.")

def ask_for_float(prompt):
    """Keep asking until the user types a number, then return it."""
    while True:
        text = input(prompt)
        try:
            return float(text)
        except ValueError:
            print("Please type a number, such as 12.50")

def ask_for_date(prompt):
    """Keep asking until the user types a date like 2026-12-25, then return it."""
    while True:
        text = input(prompt)
        try:
            return date.fromisoformat(text)
        except ValueError:
            print("Please use the form YYYY-MM-DD, for example 2026-12-25.")

def read_lines(filename):
    """Return the lines of a file as a list, without the newline characters."""
    lines = []
    with open(filename) as file:
        for line in file:
            lines.append(line.strip())
    return lines

if __name__ == "__main__":
    print("Testing helpers.py")
    number = ask_for_number("A whole number: ")
    price = ask_for_float("A price: ")
    when = ask_for_date("A date: ")
    print(f"Got {number}, {price} and {when}. All good.")

Three details:

  • The string at the very top is a docstring for the module, the same idea as a docstring for a function. It says what the file is for and how to use it.
  • The functions are exactly the ones you have written before, docstrings and all. ask_for_date() needs date, so the module has its own from datetime import date. A module imports what it needs, like any other file.
  • The block at the bottom starts with if __name__ == "__main__":. From the last section you can read that as only if this file is being run directly. So running helpers.py on its own runs a small test of its functions, and importing it runs nothing but the def lines. This is the standard place to put a module's own test or demo.

Run it directly, to see the test:

Testing helpers.py
A whole number: x
Please type a whole number.
A whole number: 7
A price: 2.5
A date: 2026-12-25
Got 7, 2.5 and 2026-12-25. All good.

Using your module

Save a new program in the same folder as helpers.py, and both kinds of import work:

use_helpers.py
import helpers

width = helpers.ask_for_float("Width of the room: ")
length = helpers.ask_for_float("Length of the room: ")
print(f"The floor area is {round(width * length, 1)} square metres.")
Width of the room: 4
Length of the room: 3.5
The floor area is 14.0 square metres.
from_helpers.py
from helpers import read_lines, ask_for_number

lines = read_lines("events.txt")
number = ask_for_number(f"Which line, 1 to {len(lines)}? ")
print(lines[number - 1])
Which line, 1 to 5? 2
2027-01-01,New Year's Day

Nothing printed from the module this time, because the test is behind if __name__ == "__main__":. Notice how short these programs are. Asking for input safely, which took seven lines in Module 7, is now one line, and it is the same tested line in every program you write from now on. If you ever improve ask_for_number(), every program improves.

The advice from lesson 1 applies to your own modules too: import helpers and helpers.ask_for_float(...) is the safe default, and from helpers import ask_for_float reads well when you use one or two names a lot. The names here are clear on their own, so this lesson mostly uses the second form.

Since the functions have docstrings, help() works on your module just as it does on Python's:

explore_helpers.py
import helpers

help(helpers)
Help on module helpers:

NAME
    helpers - Small functions for asking the user for input and reading files.

DESCRIPTION
    Import this from any program saved in the same folder:
        from helpers import ask_for_number

FUNCTIONS
    ask_for_date(prompt)
        Keep asking until the user types a date like 2026-12-25, then return it.

    ask_for_float(prompt)
        Keep asking until the user types a number, then return it.

    ask_for_number(prompt)
        Keep asking until the user types a whole number, then return it.

    read_lines(filename)
        Return the lines of a file as a list, without the newline characters.

That is your own documentation, generated from the docstrings you wrote. The habit from Module 5 has paid off.

Where Python looks

When you write import helpers, Python searches a short list of places, in order. The first is the folder of the file being run. After that come the folders where the standard library lives. This explains two things you have met:

  • helpers.py must sit in the same folder as the program that imports it. Move the program to another folder, and the import fails with ModuleNotFoundError: No module named 'helpers', even though the file still exists.
  • Naming your own file random.py breaks import random, as Module 3 warned, because your folder is searched before the standard library. The same goes for math.py, string.py, datetime.py, and, less obviously, test.py, which is also the name of a standard library module. Give your modules names that describe what is in them.

Project: the countdown, rebuilt

The countdown from last lesson, rebuilt on top of helpers.py, and with a menu that lets you add events from inside the program instead of editing the file:

countdown2.py
from datetime import date
from helpers import read_lines, ask_for_date

FILENAME = "events.txt"

def describe(event_date, name, today):
    """Return one line saying how far away the event is."""
    days = (event_date - today).days
    if days == 0:
        return f"{name} is today!"
    elif days > 0:
        return f"{name} is in {days} days."
    else:
        return f"{name} was {-days} days ago."

def show_events(today):
    for line in read_lines(FILENAME):
        text, name = line.split(",")
        try:
            event_date = date.fromisoformat(text)
        except ValueError:
            print(f"Skipping '{name}': '{text}' is not a date.")
            continue
        print(describe(event_date, name, today))

def add_event():
    name = input("Name of the event: ")
    event_date = ask_for_date("Date (YYYY-MM-DD): ")
    with open(FILENAME, "a") as file:
        file.write(f"{event_date},{name}\n")
    print(f"Added {name}.")

today = date.today()
print(f"Today is {today.strftime('%A %d %B %Y')}.")

while True:
    print()
    print("1. Show countdowns")
    print("2. Add an event")
    print("3. Quit")
    choice = input("Choose: ")
    if choice == "1":
        show_events(today)
    elif choice == "2":
        add_event()
    elif choice == "3":
        break
    else:
        print("Please choose 1, 2 or 3.")
Today is Saturday 19 September 2026.

1. Show countdowns
2. Add an event
3. Quit
Choose: 2
Name of the event: Birthday
Date (YYYY-MM-DD): 2026-11-05
Added Birthday.

1. Show countdowns
2. Add an event
3. Quit
Choose: 1
Christmas is in 97 days.
New Year's Day is in 104 days.
Started the Python course was 9 days ago.
Skipping 'Holiday': 'soon' is not a date.
Halloween is in 42 days.
Birthday is in 47 days.

1. Show countdowns
2. Add an event
3. Quit
Choose: 3

read_lines() and ask_for_date() are imported rather than defined, so a program that gained a menu and a whole add_event() function grew by only fifteen lines. add_event() writes the date with an f-string, which gives the ISO form, so the file stays readable by fromisoformat(), as promised last lesson.

Try it

Make a folder for your own projects, if you do not have one, and put helpers.py in it. Then take three programs from earlier modules that use int(input(...)) or read a file, save them in that folder, and rewrite them to import from helpers. Count how many lines each one loses.

Common mistakes

ModuleNotFoundError: No module named 'helpers'

The program and helpers.py are not in the same folder. It is the folder of the program file that matters, not the folder your terminal is in. Check the spelling too: the import name is the filename without .py.

ImportError: cannot import name 'ask_for_num' from 'helpers'

The module was found, but has no function by that name. Check the spelling against the def line in the module. Python suggests the closest match: Did you mean: 'ask_for_number'?

NameError: name 'ask_for_number' is not defined

You wrote import helpers and then called ask_for_number(...) without the helpers. in front. Either add it, or import the name directly with from helpers import ask_for_number.

The module prints things or asks questions when I import it

Its top level has code other than definitions. Move that code under if __name__ == "__main__":, or into a function.

I changed helpers.py but my program still behaves the old way

If you are at the Python prompt, it loaded the module once and keeps that copy. Restart the prompt. If you are running a file, check that you saved helpers.py, and that the program is importing the copy you edited and not another helpers.py in a different folder.

My file is called test.py and imports do strange things

test is a standard library module, so is code, and so are many other short names. If an import behaves oddly, rename your file to something more specific.

Exercises

  1. Yes or no. Add ask_yes_no(prompt) to helpers.py. It should keep asking until the user types y or n, in either case, and return True for yes and False for no. Add a line to the module's test for it.
  2. Tip calculator, rebuilt. Rewrite the Module 7 tip calculator to import ask_for_float from helpers instead of defining it.
  3. Shapes module. Write shapes.py with rectangle_area(), circle_area() and triangle_area(), each with a docstring, and a test under if __name__ == "__main__": that prints a result you can check by hand for each. Then write a separate program that imports it and asks the user for a radius.
  4. Split the to-do app. Take todo_safe.py from Module 7 and move load_tasks() and save_tasks() into a module called todo_storage.py. The main program should import them, and import ask_for_number from helpers. It should behave exactly as before, with less code in it.
Solution 1
# Add this function to helpers.py, above the if __name__ == "__main__": line.

def ask_yes_no(prompt):
    """Keep asking until the user answers y or n. Return True for yes."""
    while True:
        answer = input(prompt + " (y/n) ").strip().lower()
        if answer == "y":
            return True
        elif answer == "n":
            return False
        print("Please answer y or n.")

if __name__ == "__main__":
    if ask_yes_no("Do you like Python?"):
        print("Good choice.")
    else:
        print("Give it time.")

.strip().lower() means Y, n and y are all accepted. The function returns a boolean, so callers can write if ask_yes_no("Play again?"): and nothing else.

Solution 2
# Save this next to helpers.py.
from helpers import ask_for_float

bill = ask_for_float("Bill: ")
percent = ask_for_float("Tip percent: ")
tip = bill * percent / 100
print(f"Tip: {round(tip, 2)}")
print(f"Total: {round(bill + tip, 2)}")

Seven lines of the original became one from line. The behaviour is identical.

Solution 3
"""Area calculations for simple shapes. Save this as shapes.py."""

import math

def rectangle_area(width, height):
    """Return the area of a rectangle."""
    return width * height

def circle_area(radius):
    """Return the area of a circle."""
    return math.pi * radius ** 2

def triangle_area(base, height):
    """Return the area of a triangle."""
    return base * height / 2

if __name__ == "__main__":
    print(rectangle_area(3, 4))       # expect 12
    print(round(circle_area(1), 2))   # expect 3.14
    print(triangle_area(10, 5))       # expect 25.0

The test prints values with the expected answers alongside as comments, so a glance shows whether anything broke. The program that uses it is two lines after the import: from shapes import circle_area and a print.

Solution 4

First the storage module:

"""Loading and saving for the to-do app. Nothing here knows about menus."""

FILENAME = "todo.txt"

def load_tasks():
    """Return the saved tasks as a list, or an empty list if there is no file yet."""
    tasks = []
    try:
        with open(FILENAME) as file:
            for line in file:
                tasks.append(line.strip())
    except FileNotFoundError:
        print("No saved tasks yet. Starting a new list.")
    return tasks

def save_tasks(tasks):
    """Write the tasks to the file, one per line."""
    with open(FILENAME, "w") as file:
        for task in tasks:
            file.write(task + "\n")

Then the main program:

# Save this next to todo_storage.py and helpers.py.
from todo_storage import load_tasks, save_tasks
from helpers import ask_for_number

def show_tasks(tasks):
    if len(tasks) == 0:
        print("Nothing to do!")
    number = 1
    for task in tasks:
        print(f"{number}. {task}")
        number += 1

def finish_task(tasks):
    if len(tasks) == 0:
        print("Nothing to finish!")
        return
    show_tasks(tasks)
    number = ask_for_number("Which number is done? ")
    if number >= 1 and number <= len(tasks):
        finished = tasks.pop(number - 1)
        print(f"Finished: {finished}")
    else:
        print(f"There is no task {number}.")

def show_menu():
    print()
    print("1. Show tasks")
    print("2. Add a task")
    print("3. Finish a task")
    print("4. Quit")
    return input("Choose: ")

tasks = load_tasks()
print(f"Loaded {len(tasks)} tasks.")

while True:
    choice = show_menu()
    if choice == "1":
        show_tasks(tasks)
    elif choice == "2":
        tasks.append(input("New task: "))
    elif choice == "3":
        finish_task(tasks)
    elif choice == "4":
        save_tasks(tasks)
        print(f"Saved {len(tasks)} tasks. Goodbye.")
        break
    else:
        print("Please choose 1, 2, 3, or 4.")

The main program no longer knows the filename or how tasks are stored. That is the one job rule from Module 5 applied to whole files: todo_storage.py deals with the file, helpers.py deals with input, and the main program deals with the menu. Module 9 builds a bigger to-do app on exactly this structure.

Summary

  • Any .py file is a module. import greetings runs greetings.py and makes its functions available as greetings.name.
  • Python looks in the folder of the running program first, so put your module next to the programs that use it.
  • Importing runs the whole file, so a module's top level should contain definitions only.
  • if __name__ == "__main__": marks code that runs only when the file is run directly. Put the module's test there.
  • Give modules docstrings and help() will document them. Never name a module after a standard library module.
  • Keep helpers.py and add to it. It is the start of your own library.

Module 8 complete

You can now use Python's toolbox and build your own. Module 9 puts everything together in one larger project, a to-do app with due dates, priorities and a proper save file, built the way you would build it for real.

Next: Planning the app