# Python in MITK {#PythonInMITKPage}

[TOC]

## Overview

There are at least three closely intertwined aspects to consider when working with Python in MITK:

1. **Embedding Python** using the Python C API by including Python.h and linking against the Python library in C++.
2. **Running the Python interpreter** as a separate process, passing command-line arguments.
3. **Exposing MITK functionality to Python** via the mitk Python module, using pybind11 to generate the necessary bindings.

Each of these aspects comes with its own challenges, and their integration imposes certain restrictions and pitfalls that can affect one another.
As such, even minor changes to one of these components require careful consideration of the others.

The foundation for making all of this work is to ensure that **a consistent set of Python binaries** is used across all aspects.
These binaries and their environment must be **isolated from any system Python** or unrelated Python installations that may exist on the target platform.

Finally, the solution must function reliably across **Windows, Linux, and macOS**.

## Why Conda, Micromamba, official embedded builds, and similar solutions don't work

At first glance, solutions like Conda, Micromamba, or the officially provided embedded Python builds seem like silver bullets.
Their advertised features and documentation suggest that robust, ready-to-use options already exist — even multiple ones to choose from.

However, it's a classic "too good to be true" scenario. All of these solutions share a fundamental flaw: **they are not truly isolated**.
Instead, they are dynamically linked to various system libraries and other dependencies.

Sooner or later, you'll run into issues like failed builds or runtime crashes due to **binary-incompatible versions of shared dependencies** between MITK and the libraries of a Python package.
These libraries are loaded dynamically at runtime and may export the same symbols, leading to symbol collisions, undefined behavior, or crashes.

## Standalone Python Builds to the rescue?

The [Standalone Python Builds](https://github.com/indygreg/python-build-standalone) project provides fully statically linked Python binaries and patches the CPython build system to use relative paths instead of absolute ones.

With these changes, Python is built from source across a wide matrix of versions, platforms, and build configurations, and distributed via GitHub releases.

These builds are **fully standalone**: just download, unzip, and run them on any machine, with no additional dependencies.
They do not interfere with your project’s libraries or depend on system-level Python components, making them ideal for embedding or integration into complex applications like MITK.

The downside of this approach is that these Python builds are significantly larger in file size compared to their dynamically linked counterparts.
However, this trade-off is acceptable given the improved isolation, reliability, and portability they provide.

## Standalone Python Builds in MITK

### Superbuild integration

We integrated the Standalone Python Builds as an external project in the MITK superbuild.
When `MITK_USE_Python3` is enabled (which is the default in our standard `WorkbenchRelease` configuration), this external project:

1. Downloads the platform-appropriate archive from GitHub,
2. Extracts it into `ep/src/Python3`,
3. Upgrades `pip`, and
4. Pre-installs the `numpy` package, which is required by MITK when Python is enabled.

The project is then installed by copying `ep/src/Python3/` into `MITK-build/python`.

Since our build system relies on CMake’s `find_package()` to locate external dependencies, we enforce consistency for calls to `find_package(Python3)` by passing both `Python3_ROOT_DIR` (set to `MITK-build/python`) and `Python3_EXECUTABLE` (the interpreter inside that directory) from the superbuild down to the MITK build itself. `Python3_ROOT_DIR` alone is only a search hint; `find_package(Python3)` still consults `PATH` and the Windows registry and prefers the newest version it finds there, so on a machine with a newer system Python the hint would be overruled. Pinning `Python3_EXECUTABLE` makes the MITK build skip that search and use the staged interpreter unconditionally.

MITK modules can then depend on Python either via the classic MITK syntax `PACKAGE_DEPENDS Python3|Python` or by using the native CMake target like `TARGET_DEPENDS Python3::Python`.

### No more debug builds?

Historically, debug builds with Python have been problematic, as Python packages are rarely distributed in debug variants.
As a result, **debug builds are disabled** in MITK when `MITK_USE_Python3` is enabled.
Developers should instead use the `RelWithDebInfo` configuration for debugging.

## Python modules in MITK

Leaving Python wrapping aside for now, the Python integration in MITK is split into three modules:

1. **MitkPythonHelper**: This module **does not** link against the Python library.
It primarily offers utility functions for retrieving paths such as the Python home directory, the Python library, and the Python executable — all of which can differ between the build tree and installed versions of MITK.
It also includes functions to create, locate, and activate virtual environments.

2. **MitkPreloadPython**: This module **does not** depend on the Python library at link time.
It is automatically loaded with the **MitkCore** module. Its sole purpose is to load the correct Python library at runtime with **global symbol visibility** as early as possible to prevent later loads (e.g., via link-time dependencies of other modules) from restricting symbol visibility or accidentally loading a Python library from a different location.
It also sets and unsets a few environment variables to ensure a clean and functioning Python environment.

3. **MitkPython**: This module **does** link privately against the Python library.
It provides the `mitk::PythonContext` class, which acts as the primary bridge between MITK and Python.

In most cases, you won't need to interact with the `MitkPreloadPython` module directly.
To run the Python interpreter as a separate process, use `MitkPythonHelper`.
To exchange data (e.g., images) between MITK and Python, use `MitkPython`.

### Testing

The `MitkPython` module ships two complementary test binaries, both compiled into `MitkPythonTestDriver`:

1. **`mitkPythonContextTest`**: a CppUnit suite exercising the `mitk::PythonContext` class directly from C++ — interpreter initialization, variable exchange, `Execute()`, `ExecuteFile()`, image binding, and context isolation.
2. **`mitkPythonBindingsTest`**: a thin C++ host that spins up a dedicated `mitk_pytest` virtual environment, installs `pytest` on first run, then hands control to a pytest suite under `Modules/Python/test/pytest/`. The suite covers the `mitk` Python module's binding surface (image construction, NumPy interop, I/O, geometry, pixel types, points/vectors, auto-loaded modules) as seen from idiomatic Python.

Splitting the two lets each side use its native testing idiom: CppUnit for C++ API coverage, pytest for Python-side behavior. The `mitk_pytest` venv is reused across test runs, so the pytest install cost is paid only once per machine.

## Python Wrapping: The mitk Python module

The Python wrapping of MITK is handled by pybind11.
The bindings are defined in `Wrapping/Python/mitk/` and compiled into a native extension module (`mitk.cpXYZ-<platform>.pyd` / `.so`).

Currently, the following types and functions are exposed:

| Category | Types / Functions |
|---|---|
| **Image** | `Image` with constructor overloads (empty / numpy / file path), `initialize()`, classmethods `from_numpy()` and `load()`, `save()`, `as_numpy()`, `__array__`, geometry properties (`spacing`, `origin`, `direction`, `direction_cosines`, `ndim`, `shape`, `dtype`, `array`, `time_steps`, `time_geometry`), per-time-step accessors (`get_spacing()`, `set_spacing()`, `get_origin()`, `set_origin()`, `get_direction()`, `set_direction()`, `get_geometry()`) |
| **IO** | `IOUtil.load()`, `IOUtil.save()` |
| **Geometry** | `BaseGeometry`, `Geometry3D`, `PlaneGeometry`, `SlicedGeometry3D`, `TimeGeometry` (with `count_time_steps()`, `get_min_time_point()`, `get_max_time_point()`, `get_time_bounds()`, `time_step_to_time_point()`, `time_point_to_time_step()`, `is_valid_time_step()`, `is_valid_time_point()`, `get_geometry_for_time_step()`, `get_geometry_for_time_point()`), `ArbitraryTimeGeometry`, `ProportionalTimeGeometry` |
| **Pixel types** | `PixelType`, `make_pixel_type()` |
| **Points / Vectors** | `Point2D`, `Point3D`, `Vector2D`, `Vector3D` |
| **Exceptions** | `Exception` |
| **CppMicroServices** | `get_loaded_modules()` |

`mitk.Image` is the bound C++ class. Its constructor is overloaded by argument type so the same name handles empty construction, loading from a file, and wrapping a numpy array. `isinstance(img, mitk.Image)`, type hints, IDE autocomplete, and subclassing all work normally. Named factories `mitk.Image.from_numpy()` and `mitk.Image.load()` remain available for callers who prefer to be explicit.

Basic usage:

```python
import mitk
import numpy as np
from pathlib import Path

# Empty image (call initialize() before use)
empty = mitk.Image()
empty.initialize("float32", [64, 64, 64])

# Construct from numpy
arr = np.zeros((64, 64, 64), dtype=np.float32)
img = mitk.Image(arr, spacing=(1.0, 1.0, 2.5))

# Construct from a file path (str or pathlib.Path)
loaded = mitk.Image("output.nrrd")
loaded = mitk.Image(Path("output.nrrd"))

# In-place modification (default direct, unlocked path)
img.as_numpy(writeable=True)[32, 32, 32] = 1.0

# Read access via the array protocol
print(np.asarray(img)[32, 32, 32])  # 1.0

# Geometry access
print(img.shape, img.spacing, img.origin, img.direction)

# Persistence
img.save("output.nrrd")
```

By default, `as_numpy()` returns a *direct* numpy view that pins the underlying `mitk.Image` via a smart-pointer capsule but does **not** acquire any read/write lock. This is the preferred mode for in-process work and matches the behavior expected by `numpy.asarray()` and the `__array__` protocol. For workflows that need lock-based concurrency control (e.g. multi-threaded access from C++ and Python at the same time), pass `use_accessor=True` to safeguard image access through a `ImageReadAccessor`/`ImageWriteAccessor`-backed view, which holds the MITK accessor lock until the numpy array is garbage-collected:

```python
arr = img.as_numpy(use_accessor=True, writeable=True)
arr[5, 5, 5] = 7
del arr  # release the write accessor before re-acquiring
```

The bindings are available both within MITK applications (via the embedded Python in `MITK-build/python`) and as a standalone installable wheel (see below).

## Virtual environments

When you instantiate the `mitk::PythonContext` class, you can optionally specify the name of a virtual environment to use.
If the environment doesn’t exist yet, it will be created automatically.
This allows different MITK components — such as segmentation tools — to use isolated virtual environments, enabling them to install dependencies at runtime via pip, for example.

These virtual environments are stored in the `mitk_venvs` folder within a dedicated user-writable location:

- `%%LocalAppData%` on Windows
- `$XDG_DATA_HOME` or `$HOME/.local/share` on Linux
- `$HOME/Library/Application Support` on macOS

To avoid interference between multiple MITK versions built or installed on the same machine, we use a hash of the application path of the currently running MITK application as the top-level folder name inside `mitk_venvs`.

Virtual environments created by `mitk::PythonContext` (or the corresponding functions in the `MitkPythonHelper` module) can be listed and managed through the **Python Environments** plugin in MITK.

The `mitkPythonBindingsTest` described above relies on this mechanism and creates a dedicated `mitk_pytest` virtual environment the first time it runs.

## Python Wheel

The `mitk` Python module can be packaged as a standalone, redistributable wheel (`mitk_python-*.whl`).
This allows users to `pip install` the MITK bindings into any compatible Python environment without building MITK from source.

### What is in the wheel?

The wheel bundles:

- The compiled pybind11 extension module (`mitk.cpXYZ-<platform>.pyd` / `.so`)
- All CppMicroServices auto-load modules (IO readers/writers, model fit services, etc.)
- All native library dependencies (ITK, VTK, CppMicroServices, MITK modules, etc.), vendored via a platform-specific delocator

On import, `mitk/__init__.py` sets up the environment so that CppMicroServices auto-loading works transparently — the same IO file formats are available as in a full MITK application.

### Building the wheel

Use the `PythonWheel` build configuration, which is a headless configuration (no Qt, BlueBerry, or plugins) locked to Release builds:

```bash
cmake -S . -B ../MITK-superbuild -DMITK_BUILD_CONFIGURATION=PythonWheel
cmake --build ../MITK-superbuild
```

The SuperBuild build chains into the inner MITK build, which builds all MITK modules and then produces the wheel in `../MITK-superbuild/MITK-build/` via the `mitk_python_wheel` target (included in the default build for this configuration).

The target:
1. Installs pip packaging dependencies (`wheel` + platform delocator) into the build Python
2. Stages the bindings and auto-load modules via `cmake --install --component wheel`
3. Packs a raw wheel and repairs it with the platform delocator to bundle all native dependencies

The platform delocators are:
- **Windows**: [delvewheel](https://github.com/adang1345/delvewheel) — copies DLLs into `mitk_python.libs/`
- **Linux**: [auditwheel](https://github.com/pypa/auditwheel) — copies shared libraries into `mitk_python.libs/` and patches RPATH
- **macOS**: [delocate](https://github.com/matthew-brett/delocate) — copies dylibs into `mitk/.dylibs/` and rewrites load commands

### Testing the wheel

A self-contained smoke test script is provided:

```bash
python Wrapping/Python/wheel/test_wheel.py --build-dir ../MITK-superbuild/MITK-build
```

This automatically creates a temporary virtual environment, installs the wheel, runs the tests, and cleans up.
The test scope is deliberately narrow: it covers wheel-specific concerns (import, `__version__`, CppMicroServices auto-load bundling) plus a single functional sanity check.
Comprehensive binding coverage lives in the `mitkPythonBindingsTest` pytest suite described above — running those against the wheel would be redundant.

### Standalone usage

The `build_wheel.py` script can also be invoked manually outside of the CMake build:

```bash
python Wrapping/Python/wheel/build_wheel.py --build-dir <MITK-build>
```

The wheel is written to the build directory by default.
Use `--output-dir` to write it elsewhere, or `--skip-repair` to skip the delocator step for debugging.

## API documentation (Sphinx)

Doxygen does not handle Python well: it does not understand Google-style docstrings, dataclasses, or `typing.Literal`/union hints, and its native Python rendering undersells a typed binding surface.
For that reason, the `mitk` Python package has its own Sphinx-based documentation site, built and published independently of this C++ Doxygen site.

The Python documentation lives at <https://mitk-python.readthedocs.io/en/2026.06/>.
It is also reachable from the "Python API" tab in the top navigation bar of this Doxygen site.

### Sources

The Sphinx project sits next to the bindings, in `Wrapping/Python/docs/`:

- `conf.py`: Sphinx configuration (autodoc + napoleon + autosummary + autodoc-typehints + myst-parser + sphinx-copybutton + `sphinx_book_theme`).
- `requirements.txt`: pinned toolchain.
- `index.md`, `installation.md`, `getting_started.md`, `user_guide/*.md`, `api/index.md`: narrative pages and the autosummary-driven API reference.

The auto-generated API reference is populated by importing the freshly-built `mitk` package and reading docstrings off the compiled pybind11 extension.
Google-style docstrings (with `Args:` / `Returns:` / `Raises:` / `Examples:` sections) on each binding are the source of truth; keep them in sync when the bindings change.

### Building the docs locally

There is a dedicated CMake target:

```bash
cmake --build <build-dir> --target mitk_python_docs
```

The target depends on `mitk_python_bindings`.
On first invocation it creates a dedicated venv at `<build-dir>/mitk_python_docs_venv` (with `--system-site-packages` so the just-built `mitk` package is importable) and pip-installs the Sphinx toolchain from `requirements.txt` into that venv.
The venv is reused on subsequent runs.
This deliberately keeps the toolchain out of the embedded build Python, because on Windows the standalone Python's user-site directory is shared with the system Python and pollutes both.

The HTML lands in `<build-dir>/Documentation/Python/html/`.

The `-W` flag (warnings-as-errors) is passed to `sphinx-build`, so docstring syntax errors or duplicated definitions fail the build.
Missing-target cross-references (e.g. a stale `:py:class:` pointing at a name that no longer exists) only fail the build when `nitpicky = True` is set in `conf.py`; the default is off, so silently-broken cross-refs need to be caught at review time.

### Publishing

The published site is hosted on Read the Docs at <https://mitk-python.readthedocs.io/en/2026.06/>. A separate repository, `MITK/mitk-python-docs`, drives the build: it fetches `Wrapping/Python/docs` from here and runs `sphinx-build` against the `mitk-python` wheel installed from PyPI, so the site tracks the bindings without keeping a second copy of the sources.

### What goes where

- Consumer-facing (`pip install mitk-python`, NumPy interop, file I/O, geometry, properties) lives on the Sphinx site.
- This Doxygen page (`PythonInMITK`) is the developer-facing reference: how the wheel is built, how the C++ side embeds Python, why Standalone Python Builds, platform quirks, and so on.

The two are intentionally complementary, not duplicates.

## Quirks

As mentioned at the beginning, Python integration is a complex and sometimes fragile feature that can easily break in certain scenarios.
In particular, differences between our supported platforms—Windows, Linux, and macOS—can be challenging.
This section collects a few non-obvious quirks.

### Preloading the Python library on macOS

While we explictly unset the `PYTHONHOME` environment variable on Windows and Linux before preloading the Python library, on macOS we must explicitly set it.

### OpenMP on macOS

While Python Standalone Builds have proven to be the most reliable and isolated solution for us, we have encountered conflicts between the OpenMP libraries used by MITK and certain Python packages on macOS.
To avoid these issues, OpenMP is automatically disabled on macOS whenever Python is enabled in the build system.

### Python tests on macOS

On macOS, the test driver for the `MitkPython` module attempts to load the Python library before the `MitkPreloadPython` autoload-module has a chance to load it as intended.
This premature loading fails because, in Standalone Python Builds, library paths on macOS are prefixed with `/install/lib/`.
To work around this, we use macOS’s `DYLD_INSERT_LIBRARIES` feature to manually preload the Python library by setting the `ENVIRONMENT` test property for all Python tests.
As a result, the tests run correctly via CTest, but still abort when executed directly.

### Converting Python into a framework for packaging on macOS

To sign an application bundle on macOS with `codesign`, the bundle must follow certain rules regarding the location and format of its binaries.
Therefore, we convert the `python` directory from `MITK-build` into `Python.framework` for packaging.
This conversion is handled in the `MITK-build/FixMacOSInstaller.cmake` script, which CPack executes as a post-build step.

### Importing mitk in the Python interpreter of an installed MITK on macOS

We are using CMake's `BundleUtilities` to create application bundles on macOS.
Unfortunately, it rewrites all library dependency paths to start with `@executable_path/../MacOS`, which works fine for executables in the usual `Contents/MacOS` folder of an app bundle.
However, this breaks when the Python interpreter in `Contents/Frameworks/Python.framework/Versions/A/bin` tries to load the dependencies of the `mitk` package.

To fix this, we adjust the runtime dependency paths of the `mitk` package to use an `@loader_path` approach in the `FixMacOSInstaller.cmake` script, which runs automatically after `fixup_bundle()` has finished modifying all paths.
This fix currently does not cover autoload-modules, which is why they cannot be loaded in this scenario.
Running the Python interpreter as subprocess of an MITK application, however, will load autoload-modules correctly.
