Packaging and Project Structure
From Scripts to Packages
A simulation that lives in a single script works until you need to import it from another script, share it with a collaborator, or run it on a cluster. Proper packaging solves all three problems: it makes your code importable, installable, and distributable.
This section covers the modern Python packaging ecosystem centered
around pyproject.toml and the src layout — the approach used by
NumPy, SciPy, and most major scientific Python projects.
Definition: pyproject.toml
pyproject.toml
pyproject.toml is the unified configuration file for Python projects
(PEP 518, PEP 621). It replaces setup.py, setup.cfg, and
requirements.txt with a single, declarative file:
[build-system]
requires = ["setuptools>=68.0", "wheel"]
build-backend = "setuptools.backends._legacy:_Backend"
[project]
name = "mimo-sim"
version = "0.1.0"
description = "MIMO channel simulation toolkit"
requires-python = ">=3.11"
dependencies = [
"numpy>=1.26",
"scipy>=1.12",
"matplotlib>=3.8",
]
[project.optional-dependencies]
dev = ["pytest>=8.0", "mypy>=1.8", "ruff>=0.3"]
gpu = ["cupy>=13.0"]
Definition: The src Layout
The src Layout
The src layout places your package inside a src/ directory,
preventing accidental imports from the project root:
mimo-sim/
+-- pyproject.toml
+-- src/
| +-- mimo_sim/
| +-- __init__.py
| +-- channel.py
| +-- detection.py
| +-- utils.py
+-- tests/
| +-- test_channel.py
| +-- test_detection.py
+-- docs/
+-- examples/
With the src layout, you must install the package to import it,
which guarantees that what you test is what you ship.
Definition: Entry Points and CLI
Entry Points and CLI
Entry points let you create command-line tools from your package:
[project.scripts]
mimo-sim = "mimo_sim.cli:main"
This creates an executable mimo-sim that calls main() in
mimo_sim/cli.py:
# src/mimo_sim/cli.py
import argparse
def main():
parser = argparse.ArgumentParser(description="MIMO Simulator")
parser.add_argument("--n-rx", type=int, default=4)
parser.add_argument("--n-tx", type=int, default=2)
parser.add_argument("--snr", type=float, nargs="+", default=[0, 5, 10])
args = parser.parse_args()
# ... run simulation
Definition: Editable Install
Editable Install
An editable install (pip install -e .) creates a link from
the Python environment to your source code, so changes take effect
immediately without reinstalling:
cd mimo-sim/
pip install -e ".[dev]" # Install with dev dependencies
This is the standard development workflow:
- Clone the repository
- Create a virtual environment
pip install -e ".[dev]"- Edit code, run tests, repeat
Theorem: Python Import Resolution Order
When Python encounters import mimo_sim, it searches directories
in sys.path in order:
- The directory containing the script being run
- Directories in the
PYTHONPATHenvironment variable - The site-packages directory of the active environment
- Standard library directories
The first match wins. The src layout prevents item (1) from
shadowing the installed package, because src/mimo_sim/ is not
directly in the project root.
Without the src layout, running python from the project root
would import the local directory mimo_sim/ instead of the
installed package. This means you test un-installed code, which
may behave differently from what pip install produces.
Example: Building a Complete Scientific Package
Create a minimal but complete Python package for a MIMO channel simulator with proper project structure, type hints, and tests.
Create the project structure
mkdir -p mimo-sim/src/mimo_sim mimo-sim/tests
cd mimo-sim
Write pyproject.toml
[build-system]
requires = ["setuptools>=68.0"]
build-backend = "setuptools.backends._legacy:_Backend"
[project]
name = "mimo-sim"
version = "0.1.0"
requires-python = ">=3.11"
dependencies = ["numpy>=1.26", "scipy>=1.12"]
[project.optional-dependencies]
dev = ["pytest>=8.0", "mypy>=1.8"]
[project.scripts]
mimo-sim = "mimo_sim.cli:main"
[tool.setuptools.packages.find]
where = ["src"]
[tool.mypy]
strict = true
[tool.pytest.ini_options]
testpaths = ["tests"]
Write the package code
# src/mimo_sim/__init__.py
from mimo_sim.channel import make_rayleigh_channel
from mimo_sim.detection import zf_detect
__all__ = ["make_rayleigh_channel", "zf_detect"]
__version__ = "0.1.0"
Install and test
python -m venv .venv
source .venv/bin/activate
pip install -e ".[dev]"
pytest -v
Example: Building a CLI for Simulation Sweeps
Create a command-line interface that runs BER simulations over a range of SNR values and saves results to a CSV file.
Write the CLI module
# src/mimo_sim/cli.py
import argparse
import csv
import numpy as np
from mimo_sim.channel import make_rayleigh_channel
from mimo_sim.detection import zf_detect
def main():
parser = argparse.ArgumentParser(
description="MIMO BER Simulation"
)
parser.add_argument("--n-rx", type=int, default=4)
parser.add_argument("--n-tx", type=int, default=2)
parser.add_argument(
"--snr", type=float, nargs="+",
default=[0, 5, 10, 15, 20],
)
parser.add_argument("--trials", type=int, default=1000)
parser.add_argument("-o", "--output", default="results.csv")
args = parser.parse_args()
results = []
for snr in args.snr:
ber = run_trial(args.n_rx, args.n_tx, snr, args.trials)
results.append({"snr_db": snr, "ber": ber})
print(f"SNR={snr:6.1f} dB BER={ber:.4e}")
with open(args.output, "w", newline="") as f:
writer = csv.DictWriter(f, fieldnames=["snr_db", "ber"])
writer.writeheader()
writer.writerows(results)
print(f"Results saved to {args.output}")
Run from the command line
mimo-sim --n-rx 8 --n-tx 4 --snr 0 5 10 15 20 --trials 5000 -o ber.csv
Package Structure Visualization
An animated walkthrough of how a Python project grows from a single script to a properly packaged project with src layout, tests, and CLI.
Parameters
Why This Matters: Packaging in 5G NR Research
5G NR physical layer research involves multiple interacting modules: LDPC encoding, OFDM modulation, channel estimation, MIMO detection, and link adaptation. Packaging each module as an installable library with typed interfaces enables independent development and testing. The O-RAN Software Community (OSC) uses exactly this pattern: separate packages for the DU, CU, and RIC components, integrated via well-defined APIs.
See full treatment in Chapter 15
Common Mistake: Flat Layout Without src/
Mistake:
Placing the package directly in the project root:
mimo-sim/
+-- mimo_sim/
+-- tests/
+-- pyproject.toml
Running python from the project root imports the local directory
instead of the installed package, causing tests to pass locally
but fail after installation.
Correction:
Use the src layout:
mimo-sim/
+-- src/
| +-- mimo_sim/
+-- tests/
+-- pyproject.toml
Configure setuptools to find packages in src/:
[tool.setuptools.packages.find]
where = ["src"]
Quick Check
What does pip install -e . do?
Installs the package system-wide
Creates a link so code changes take effect without reinstalling
Exports the package as a wheel file
Runs the package's test suite
An editable install links the environment to your source directory.
Key Takeaway
Use pyproject.toml and the src layout. This is the modern
standard for Python packaging. It ensures that tests run against
the installed package, not loose source files, and makes your code
installable with a single pip install -e ".[dev]".
pyproject.toml
The unified Python project configuration file that declares build system, project metadata, dependencies, and tool settings.
Related: editable install
editable install
An installation mode (pip install -e .) that creates a symlink
to the source code so changes are immediately available without
reinstalling.
Related: pyproject.toml
Project Scaffolding Script
# Code from: ch04/python/project_scaffold.py
# Load from backend supplements endpoint