Learning Goals
3 minBy the end of this lesson you can:
- Save a function in
helpers.pyand import it inmain.pywithfrom helpers import .... - Explain what
__name__ == "__main__"guards against. - Pick what belongs in a module (reusable functions) vs what belongs in the main script (the run order).
- Run a script from the terminal and confirm both files are in the right place.
Warm-Up · A Story of Two Files
5 minMake a fresh folder. Inside, create two files side by side:
helpers.py
def greet(name): return f"Hello, {name}!" def double(x): return x * 2
main.py
from helpers import greet, double print(greet("Aisyah")) print(double(7))
Run main.py. You should see:
Hello, Aisyah! 14
You just made your own module. helpers is a Python file Python can import — just like math or random — except you wrote it.
Any .py file is a module. Save useful functions in one file, import them from another. That's how every real Python program is organised.
New Concept · Splitting Your Code
14 minHow import finds your file
When you write from helpers import greet, Python looks for helpers.py in three places, in this order:
- The folder of the file you're running.
- The folders in the
PYTHONPATHenvironment variable. - The standard-library and site-packages folders.
For today, step 1 is all that matters. Put both files in the same folder. Run the main file. It works.
What to put in a module
- Functions you might want to use from many places.
- Constants you reuse — colour codes, file paths, magic numbers.
- Classes (Level 3).
- Not anything that runs immediately (input prompts, prints, game loops) — those belong in the main script.
The __name__ == "__main__" trick
Sometimes you want a file to also be runnable on its own — for testing, demos, or quick scripts. The convention:
# helpers.py def greet(name): return f"Hello, {name}!" def double(x): return x * 2 # Optional demo when this file is run directly if __name__ == "__main__": print(greet("anyone")) print(double(21))
Python sets the special variable __name__ to the string "__main__" only when this file is the one being run. When the file is imported, __name__ is the module's name instead — "helpers". So:
python helpers.py → __name__ is "__main__" → demo runs python main.py (which imports helpers) → __name__ is "helpers" → demo skipped
This pattern is so common in Python that you'll see it in almost every real project file. Use it for self-test code or a tiny CLI demo.
The two-file shape
For most projects:
my_project/ ├─ helpers.py ← shared functions, no game logic ├─ main.py ← the game / app — imports from helpers └─ scores.txt ← data files
A common bug · circular imports
If a.py imports b.py and b.py imports a.py, Python gets stuck in a loop. The fix is usually to move the shared code into a third file c.py that both import from. We'll meet this properly in Level 3 — for now, keep your imports flowing in one direction only.
Worked Example · safe_inputs + a Game
12 minThe story
You wrote safe_inputs.py as homework for PY-L2-27. Today, use it from a separate game file. The same module will serve every future game you write.
safe_inputs.py
# safe_inputs.py — reusable input helpers def ask_int(prompt, lo=None, hi=None): while True: try: n = int(input(prompt)) except ValueError: print(" ! Whole numbers only.") continue if lo is not None and n < lo: continue if hi is not None and n > hi: continue return n def ask_yes_no(prompt): while True: s = input(prompt).strip().lower() if s in ("y", "yes"): return True if s in ("n", "no"): return False print(" ! Please answer yes or no.") if __name__ == "__main__": print("safe_inputs.py demo") n = ask_int("Pick a number 1-10: ", 1, 10) again = ask_yes_no("Was that easy? (y/n) ") print(f"You picked {n}; was it easy? {again}")
guesser.py
# guesser.py — number-guessing game using safe_inputs import random from safe_inputs import ask_int, ask_yes_no def play_one(): target = random.randint(1, 100) guesses = 0 while True: g = ask_int("Guess 1-100: ", 1, 100) guesses += 1 if g == target: print(f"Got it in {guesses} guesses!") return guesses elif g < target: print("Higher.") else: print("Lower.") while True: play_one() if not ask_yes_no("Play again? (y/n) "): print("Bye!") break
Run guesser.py. It uses safe_inputs without copying any code from it. If you find a bug in ask_int, you fix it once and every game that imports it benefits.
Run safe_inputs.py directly. The __main__ demo runs. When guesser.py imports it, the demo does NOT run — only the function definitions get pulled in.
The game file is now shorter and clearer than before. There's no input-validation noise — it's all hidden inside safe_inputs. The game file only contains game logic. That separation is what every real codebase looks like.
Try It Yourself
13 minCreate math_helpers.py with three functions: square(n), cube(n), is_even(n). From a separate test.py, import and call all three.
Hint
# math_helpers.py def square(n): return n * n def cube(n): return n * n * n def is_even(n): return n % 2 == 0 # test.py from math_helpers import square, cube, is_even print(square(5), cube(5), is_even(5)) # → 25 125 False
Add a __main__ block to your math_helpers.py that prints a tidy table of squares and cubes for 1-10 when you run the file directly. Confirm test.py still works without the demo running.
Hint
# at the bottom of math_helpers.py if __name__ == "__main__": print(f"{'n':<3}{'n²':>5}{'n³':>6}") for n in range(1, 11): print(f"{n:<3}{square(n):>5}{cube(n):>6}")
Run the file directly — see the table. Run test.py — three numbers, no table. The guard works.
Create colours.py that exports four ANSI constants (RED, GREEN, YELLOW, RESET). Use it from a separate file to print three coloured words.
Hint
# colours.py RED = "\033[91m" GREEN = "\033[92m" YELLOW = "\033[93m" RESET = "\033[0m" # rainbow.py from colours import RED, GREEN, YELLOW, RESET print(f"{RED}stop{RESET}, {YELLOW}slow{RESET}, {GREEN}go{RESET}")
Modules aren't only for functions — constants live in modules too. You used a constants block in PY-L2-25 (Wordle); this is the same idea, split across files.
Mini-Challenge · Split Your Wordle
8 minTake your wordle_final.py from PY-L2-25 and break it into three files in a wordle/ folder:
wordle/colours.py— the four ANSI constants.wordle/wordlogic.py—load_word_bank,feedback,save_result,lifetime_stats.wordle/play.py— theshow_row,render_board,play_one, and the main run loop. Imports from the other two.
Run play.py. The game should work exactly as before.
Stretch goal. Add a if __name__ == "__main__": demo to wordlogic.py that runs the feedback tests from PY-L2-24's homework — so running wordlogic.py directly prints the test results.
Show the structure
# wordle/colours.py GREEN = "\033[97;42m" YELLOW = "\033[30;43m" GREY = "\033[97;100m" RESET = "\033[0m" # wordle/wordlogic.py def load_word_bank(path): with open(path, encoding="utf-8") as f: return [line.strip().lower() for line in f if line.strip()] def feedback(secret, guess): # ...two-pass algorithm from PY-L2-24... def save_result(secret, history, won): # ... def lifetime_stats(): # ... if __name__ == "__main__": tests = [("peach", "place", ["G", ".", "G", "G", "Y"]), ...] passing = sum(1 for s, g, e in tests if feedback(s, g) == e) print(f"{passing}/{len(tests)} passing") # wordle/play.py from colours import GREEN, YELLOW, GREY, RESET from wordlogic import load_word_bank, feedback, save_result, lifetime_stats # ... rest of the game ...
Non-negotiables: three files, no logic duplicated, play.py works exactly as the original. The wordlogic demo proves that file alone tests its own feedback function — beautiful separation.
Recap
3 minAny .py file is a module — name it, import from it, reuse it from many places. Put functions and constants in modules; put the run order in the main script. The if __name__ == "__main__": guard lets a file act as both a library and a runnable script. Keep your modules small and focused — one job per file makes everything easier to find later. Big programs are dozens of small files, not one giant one.
Vocabulary Card
- module
- A
.pyfile that can be imported. - __name__
- A built-in variable. Equals
"__main__"when the file is run directly; equals the module name when imported. - __name__ == "__main__" guard
- An
ifblock at the bottom of a file that runs only when the file is the main script. - circular import
- When A imports B and B imports A. Python gets stuck. Fix by extracting shared code into a third module.
Homework
4 minRe-organise one of your earlier games (high-score keeper from PY-L2-22, or the calculator from PY-L2-29) into two files:
storage.py— every function that reads or writes the disk.app.py— every function that talks to the user, and the main loop. Imports fromstorage.
Add a __main__ demo to storage.py that loads the data and prints a summary — for testing without the game.
Sample structure (using high scores)
# storage.py FILE = "scores.txt" def load_scores(): try: with open(FILE, encoding="utf-8") as f: return [{"name": n.strip(), "score": int(s.strip())} for line in f if line.strip() for n, s in [line.split(",")]] except FileNotFoundError: return [] def save_scores(scores): with open(FILE, "w", encoding="utf-8") as f: for s in scores: f.write(f"{s['name']},{s['score']}\n") if __name__ == "__main__": rows = load_scores() print(f"{len(rows)} score(s) on file") for r in rows: print(f" {r['name']}: {r['score']}") # app.py from storage import load_scores, save_scores scores = load_scores() # ... game-style usage of load/save ...
Non-negotiables: two files, no logic duplicated, a __main__ demo in storage.py, and app.py imports the functions cleanly. Real applications grow this pattern: storage, logic, ui, config — one module per concern.