Turning a folder of .py files into a package you can import reliably. The goal is structure that survives moving from ‘script on my laptop’ to ‘project I pick up again in six months’.
A sensible folder layout
Start small. A single package with tests beside it is enough for most learning projects and many production utilities.
mytool/
├── pyproject.toml
├── README.md
├── src/
│ └── mytool/
│ ├── __init__.py
│ ├── cli.py
│ └── helpers.py
└── tests/
└── test_helpers.py
The src/ layout keeps import mistakes honest: you test the installed package, not accidental imports from the project root.
What goes in __init__.py
__init__.py marks a directory as a package. It can be empty, or it can expose a tidy public API.
# src/mytool/__init__.py
from .helpers import normalise_email
__all__ = ["normalise_email"]
__version__ = "0.1.0"
__all__ documents what you consider public. It also limits from mytool import *, which nobody should use in serious code anyway.
pyproject.toml basics
Modern Python projects declare metadata and build settings in pyproject.toml. You do not need every field on day one.
[project]
name = "mytool"
version = "0.1.0"
description = "Small utilities for tidying contact data"
readme = "README.md"
requires-python = ">=3.10"
dependencies = []
[project.scripts]
mytool = "mytool.cli:main"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
The [project.scripts] entry creates a command-line entry point when the package is installed. We use that pattern again in the CLI tutorials.
Editable installs while you develop
Install your package in editable mode so changes to source files are picked up immediately without reinstalling.
python -m venv .venv
source .venv/bin/activate # Windows: .venvScriptsactivate
pip install -e ".[dev]"
The -e flag means editable. Quote the path so your shell does not misread the brackets.
Modules versus scripts
A script runs top to bottom when executed. A module defines behaviour other code imports. Keep reusable logic in modules; keep thin wrappers in scripts or CLI entry points.
# src/mytool/helpers.py
def normalise_email(value: str) -> str:
return value.strip().lower()
# src/mytool/cli.py
from mytool.helpers import normalise_email
def main() -> None:
print(normalise_email(" Alice@Example.com "))
if __name__ == "__main__":
main()
Naming and imports
Package names should be short, lowercase, and unique enough for PyPI if you ever publish. Import your own code with absolute imports from the package root.
# good
from mytool.helpers import normalise_email
# avoid in packaged code
from ..helpers import normalise_email
Packaging is mostly discipline: one obvious place for each concern, a version number you bump, and imports that work the same on your machine and a colleague’s. Next: virtual environments and pinning dependencies so installs reproduce reliably.

