Organizing a Python Project
Structure code you'll come back to
In this lesson
Organising a Python project means splitting your code into modules and folders with a sensible layout, so it’s easier to navigate, reuse, and return to months later. Once a program grows past a single file, this structure matters — this capstone ties together modules, environments, and good habits.
Explain it like I’m 5
Organising a project is like tidying a workshop: tools grouped sensibly, a label on the door, and a list of what you need. You can find things instantly and so can anyone who visits.
Split code into modules by responsibility
# data.py — knows how to get the data
def load_scores():
return [88, 72, 95]
# main.py — uses data.py, stays focused on the flow
import data
scores = data.load_scores()
print("Average:", sum(scores) / len(scores))
Average: 85.0
A sensible folder layout
A typical small project has a top-level folder with your code, a README.md explaining what it is and how to run it, a requirements.txt listing dependencies, a .gitignore (including .venv/), and perhaps a tests/ folder. You don’t need all of this on day one, but knowing the shape helps you grow into it.
wordcounter/
wordcount.py # the program
data.py # helper module
requirements.txt # pinned dependencies
README.md # what it is and how to run it
.gitignore # lists .venv/ so it isn't committed
.venv/ # the virtual environment (ignored)
The if __name__ == '__main__' guard
Code at the top level of a file runs whenever the file is imported, which is usually not what you want. Wrapping your ‘run the program’ code in if __name__ == "__main__": means it only runs when the file is executed directly, not when imported. This lets a file be both a reusable module and a runnable script.
# greet.py — can be imported OR run directly
def greet(name):
return f"Hello, {name}!"
def main():
print(greet("world"))
if __name__ == "__main__":
main()
Hello, world!
Add a __main__ guard so main() runs when the file is executed directly. Running it should print the greeting.
def greet(name):
return f"Hello, {name}!"
def main():
print(greet("Python"))
# add the guard so main() runs here
Add the line if __name__ == "__main__": and call main() indented beneath it.
def greet(name):
return f"Hello, {name}!"
def main():
print(greet("Python"))
if __name__ == "__main__":
main()
Hello, Python!
Habits that keep it maintainable
Use a virtual environment per project; keep a requirements.txt current; write a short README so future-you knows how to start it; give files, functions, and variables clear names. None of these are hard individually — together they’re the difference between a project you can pick back up and one you dread reopening.
Tools that keep quality high
As projects grow, a handful of tools keep them healthy — all optional, but worth knowing they exist. Formatters like black and linters like ruff automatically tidy your style and flag likely mistakes, so you stop debating spacing. A tests/ folder with automated tests lets you change code without fear of breaking it. And type hints document the types you expect and let tools catch mismatches early.
None of these are required to write Python — but each makes a project you return to far easier to trust. That’s a fitting place to finish the beginner path: from here, the best next step is to build things. Try the projects, which put these fundamentals to work.
# Type hints document the expected types; Python doesn't enforce them,
# but editors and linters use them to catch mistakes early.
def greet(name: str, times: int = 1) -> str:
return (f"Hi {name}! " * times).strip()
print(greet("Ada"))
print(greet("Bo", 3))
Hi Ada! Hi Bo! Hi Bo! Hi Bo!
Common mistake: Top-level code that runs on import
Code written at the module’s top level executes every time the file is imported, causing surprising output or side effects.
Move ‘run the program’ code into a main() function and call it from inside an if __name__ == "__main__": guard.
Common mistake: Letting one file grow without limit
It’s easy to keep adding to a single script until it’s hundreds of lines and hard to navigate.
Split related functions into separate modules by responsibility, and import between them. Each file should have a clear, single purpose.
What does if __name__ == "__main__": do?
The guard lets a file act as both an importable module and a runnable script.
Why split a program into multiple modules?
Grouping related code into focused modules keeps a growing project navigable.
Mini exercise (medium)
Sketch a folder layout for a small ‘word counter’ project: where would the code, the dependency list, the readme, and the ignored virtual environment go? Then describe what the __main__ guard in the main file is for.
The folder layout is something you sketch on paper — but the __main__ guard is real code. Add it so main() runs only when this file is the program being run.
def main():
return "word counter ran"
result = None
# TODO: set result = main(), but only when this file is run directly
Think: one folder containing wordcount.py, requirements.txt, README.md, .gitignore, and .venv/ (ignored).
def main():
return "word counter ran"
result = None
if __name__ == "__main__":
result = main()
print(result)
assert result == "word counter ran", "call main() inside the __main__ guard"
print("✓ Guard works!")