Organising a Python Project
About 20 minutes
Why this matters
One file is fine for a script. Once a project has a few files, tests and dependencies, a standard layout keeps everyone (including future you) from getting lost, and makes it installable.
The idea
A module is one .py file. A package is a folder of modules with an __init__.py file. A common modern layout:
my-tool/
βββ README.md
βββ pyproject.toml # project name, version, dependencies
βββ src/
β βββ my_tool/ # the package: lowercase_with_underscores
β βββ __init__.py # can be empty
β βββ cli.py
β βββ __main__.py # runs with: python -m my_tool
βββ tests/
βββ test_cli.py
Import across files with the package name: from my_tool.cli import main, or relatively inside the package: from .cli import main. The project folder may have a hyphen, but the package name may not (it must be a valid identifier), so my-tool becomes my_tool.
A minimal pyproject.toml:
[project]
name = "my-tool"
version = "0.1.0"
requires-python = ">=3.11"
Try it
Sketch (on paper) the layout of the expense tracker project from Module 6. Which parts would be separate modules?
Your turn
Write two functions:
-
package_name(project_name): turn a project name into a valid package name: lower-case, spaces and hyphens become underscores, other punctuation is removed."My Cool-Tool"becomes"my_cool_tool". -
scaffold_project(root, name): create the project folderroot/namecontaining exactly these files, and return its path (aPath):README.mdwith the text# NAMEand a newlinepyproject.tomlwith the four lines shown above, usingNAMEas the project name, ending with a newlinesrc/PACKAGE/__init__.py, empty (PACKAGEispackage_name(name))tests/test_smoke.pycontainingdef test_smoke():andassert Trueon two lines, ending with a newline
If the project folder already exists, raise
FileExistsErrorinstead of overwriting anything.
Recap
- A module is a file; a package is a folder with
__init__.py. - Put code in
src/package_name/, tests intests/, metadata inpyproject.toml. - Package names use underscores; project names may use hyphens.
Common mistakes
- A package folder without
__init__.py(in simple cases Python copes, but tools and imports can behave differently). - Circular imports:
a.pyimportsb.pywhich importsa.pygivesImportError. Move shared code into a third module. - Running a package file directly instead of with
python -m my_tool.cli, which breaks relative imports.