When your collection of helper functions, simulation utilities, plotting scripts, and halfβforgotten experimental snippets starts ballooning into a maze of files like utils_new_final_v3_reallyfinal.py, and you keep gluing paths together with sys.path.append(...), thatβs usually a sign: itβs time to turn your growing toolbox into a proper Python module.
This is not overkill (even for small research projects). A clean module structure makes your work reproducible, easier to share, and much easier to maintain as your simulations, datasets, and ideas evolve. In theoretical or computational sciences, where one often runs several experiments in parallel with slightly different parameters, this separation between core tools and experiment code becomes easier to maintain in the long run.
This guide will demystify the structure of a Python module and help you build your own from scratch.
A minimal, clean Python module layout looks like this:
myproject/
βββ pyproject.toml
βββ README.md
βββ src/
βββ myproject/
βββ __init__.py
βββ core.py
βββ context.py
βββ io.py
βββ physics/
βββ __init__.py
βββ core.py
βββ potentials.py
This example has a module called myproject with one submodule myproject.physics each one with several scripts.
There are two main parts to this:
Inside myproject/src
Everything that is part of your module goes inside this directory:
__init__.py fileOutside myproject/src
Here you write files that are intended for documentation, automation and package managers, e.g.
pyproject.toml file, which contains module information, requirements, python version, and declares possible CLI scripts. The pip tool uses it for installation.README.md for brief documentation, instructions and to have it nicely visualized on GithubThis structure scales well whether your project has 3 files or 300.
pyproject.tomlThe pyproject.toml is the modern configuration file for Python packages. It tells package managers how to build and install your module.
A minimal working example:
[project]
name = "myproject"
version = "0.1.0"
description = "Tools for my physics simulations"
readme = "README.md"
authors = [ { name = "Your Name" } ]
license = { text = "MIT" }
requires-python = ">=3.10"
dependencies = [
"numpy",
"scipy",
]
[build-system]
requires = ["setuptools>=61.0"]
build-backend = "setuptools.build_meta"
Here, what is important to point out are the following
name is the moduleβs name used during installation (pip install -e .).dependencies lists what your module needs to run.build-system tells installers how to build the project (using modern setuptools).setuptools_scm)[project.optional-dependencies])\_\_init\_\_.pyEvery directory that should behave as a Python package needs an __init__.py.
Its roles:
Without this file, Python wonβt import it.
Example:
# src/myproject/__init__.py
from .core import simulate, Particle
from .io import load_data
__all__ = ["simulate", "Particle", "load_data"]
This tells users:
from myproject import simulate
which objects you consider βpublicβ.
__version__ = "0.1.0"
If using setuptools_scm, this can be auto-generated.
You can re-export subpackages so users can write:
import myproject.physics as phys
If you want, I can expand the guide with:
pip install -e . for editable developmentJust tell me what to add next.