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 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 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 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

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:

  1. Clone the repository
  2. Create a virtual environment
  3. pip install -e ".[dev]"
  4. Edit code, run tests, repeat

Theorem: Python Import Resolution Order

When Python encounters import mimo_sim, it searches directories in sys.path in order:

  1. The directory containing the script being run
  2. Directories in the PYTHONPATH environment variable
  3. The site-packages directory of the active environment
  4. 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.

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.

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

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

python
A script that generates a complete project skeleton with pyproject.toml, src layout, tests directory, and CI configuration.
# Code from: ch04/python/project_scaffold.py
# Load from backend supplements endpoint