Laying out a project so the imports keep working
A layout that survives contact with reality
For anything past two files:
billing/
├── README.md
├── requirements.txt
├── .gitignore
├── run.py
├── billing/
│ ├── __init__.py
│ ├── money.py
│ └── report.py
└── tests/
└── test_money.pyThe outer billing is the project folder. The inner one is the package — a folder Python can import as a unit. run.py at the top is the entry point:
from billing.money import with_tax__init__.py
A folder becomes a package when it contains __init__.py. Since Python 3.3 a folder without one can still be imported, as a namespace package, but the rules are subtler and the failure modes are worse. Create the empty file. It costs nothing and removes a category of confusion.
__init__.py runs when the package is first imported, so it is where you re-export the public names:
# billing/__init__.py
from .money import with_tax
from .report import format_reportNow users write from billing import with_tax and you are free to move money.py later without breaking them. Keep it short; anything slow in __init__.py is paid by everyone who imports anything from the package.
Relative and absolute imports
Inside the package, one module refers to a sibling:
from .money import with_tax # relative: the dot means this package
from billing.money import with_tax # absolute: from the topRelative imports keep working if the package is renamed. Absolute ones read more clearly from outside. Pick one per project; the common convention is absolute imports everywhere except inside __init__.py.
A relative import only works inside a package, run as part of a package. This is the origin of a message thousands of people have met:
ImportError: attempted relative import with no known parent packageIt means you ran python3 billing/money.py directly. Run it as a module instead:
python3 -m billing.money-m imports the module properly, with the package context intact, and it also puts the current directory at the front of sys.path. When a project's imports work for everyone else and not for you, -m from the project root is the first thing to try.
Where tests go, and the argument about src
Tests live in a tests/ folder outside the package, and pytest from the project root finds them. Keeping them out of the package means they are not shipped to users and not imported by accident.
You will also see this variant:
project/
├── src/
│ └── billing/
└── tests/The src layout exists for one specific reason: without it, running Python from the project root puts your package on sys.path automatically, so your tests import the source folder rather than the installed package. Everything passes locally and fails after installation, because a missing file in the packaging configuration is invisible while the source is right there. With src, the only way to import your package is to install it, so the tests exercise what users get.
For a script, it is overhead. For anything you publish, it is the safer default.
Files that should not be in version control
.gitignore at minimum:
__pycache__/
*.pyc
.venv/
.env
*.sqlite3
.DS_Store.venv is machine-specific and large. .env holds secrets and is covered in its own lesson. __pycache__ is compiled output. Committing any of them creates merge conflicts nobody can resolve and, in the case of .env, a key that has to be rotated.
Configuration belongs in one place
Scatter file paths and thresholds through ten modules and changing one becomes an archaeology exercise. A single config.py holding constants, or a small function reading environment variables, keeps every knob in one file:
# billing/config.py
from pathlib import Path
DATA_DIR = Path(__file__).parent.parent / "data"
TAX_RATE = 0.18Path(__file__) is the location of the module itself, so the path is correct regardless of which folder the program was started from. Paths built from the current working directory break the moment someone runs the script from somewhere else, and that gets its own lesson shortly.
The README is part of the code
Four things, and it can be twenty lines: what this does, how to install it, how to run it, how to run the tests. Written the day the project starts, not the day somebody else needs it. Future you is somebody else.
The one thing to keep
Run a module inside a package with `python3 -m package.module` rather than by path, and build file paths from `__file__` rather than the current working directory.
Before you move on
A package works when a teammate runs `python3 -m billing.report` and fails for you with `ImportError: attempted relative import with no known parent package` when you run `python3 billing/report.py`. What is the difference?
Pick the one you would defend. Nobody sees your answer.