NA-MIC Project WeeksMany Slicer extension developers have to deal with the problem of external python dependencies: how to specify them, how and when to install them, and how to validate that the required things are installed. Everyone addresses the problem in a different way, often re-inventing the wheel and also often generating new great ideas. I’d like to collect all the best practices and turn them into a framework that is built into core slicer for extension developers to more easily grab and use. Something like “stick your dependencies in here and the use slicer.util.check_python_dependences and slicer.util.install_python_dependencies. If that turns out to be a bad idea for whatever reason, at least I can collect all the best practices and put them into the extension development documentation.
I’ve broken down the problem of external Python dependency into four components:
The table below includes all 94 extensions that are currently in the Slicer extension index and that have some external python dependencies to deal with, and my best quick guess as to how they approach three of the problems above. Here is a legend to interpret the terms I’ve put in the table:
slicer.util.pip_install.pip_install but some script or shell process that carries out installation).| Extension | Checking | Triggering | Installing |
|---|---|---|---|
| ShapeVariationAnalyzer | simple | user | isolated |
| SlicerVolBrain | simple | user | user |
| PerkTutor | simple | top level | simple |
| Q3DCExtension | simple | top level | simple |
| QuantitativeReporting | simple | top level | simple |
| SegmentationReview | simple | top level | simple |
| Slicer-ABLTemporalBoneSegmentation | simple | top level | simple |
| Slicer-ASLtoolkit | simple | top level | simple |
| Slicer-MusculoskeletalAnalysis | simple | processing | simple |
| Slicer-PET-MUST-segmenter | simple | top level | simple |
| Slicer-TITAN | simple | processing | simple |
| SlicerANTsPy | simple | top level | simple |
| SlicerAnatomyCarve | simple | top level | simple |
| SlicerArduinoController | simple | top level | simple |
| SlicerAuto3dgm | simple | top level | simple |
| SlicerAutomatedDentalTools | version | processing | simple |
| SlicerAutoscoperM | simple | processing | simple |
| SlicerBigImage | simple | processing | simple |
| SlicerBiomech | simple | processing | simple |
| SlicerBreastUltrasoundAnalysis | simple | top level | simple |
| SlicerBreast_DCEMRI_FTV | simple | top level | simple |
| SlicerCADSWholeBodyCTSeg | simple | processing | simple |
| SlicerCBCTToothSegmentation | simple | processing | simple |
| SlicerCineTrack | simple | processing | simple |
| SlicerColoc-Z-Stats | simple | processing | simple |
| SlicerConnectToSupervisely | simple | processing | simple |
| SlicerDBSCoalignment | simple | processing | simple |
| SlicerDICOMwebBrowser | simple | top level | simple |
| SlicerDMRI | simple | processing | simple |
| SlicerDebuggingTools | simple | processing | simple |
| SlicerDensityLungSegmentation | simple | processing | simple |
| SlicerDentalModelSeg | version | processing | isolated, blocking-prevention |
| SlicerFreeSurfer | simple | top level | simple |
| SlicerHDBrainExtraction | simple | processing | simple |
| SlicerHeadCTDeid | simple | processing | simple |
| SlicerHeart | simple | processing | simple |
| SlicerIDCBrowser | simple | top level | display |
| SlicerIVIMFit | simple | processing | simple |
| SlicerImageAugmenter | simple | button | simple |
| SlicerJupyter | simple | processing | simple |
| SlicerKonfAI | version | top level | blocking-prevention |
| SlicerLungCTAnalyzer | simple | processing | simple |
| SlicerMEMOS | simple | processing | simple |
| SlicerMHubRunner | simple | top level | simple |
| SlicerMONAIAuto3DSeg | version | processing | blocking-prevention |
| SlicerMONAIViz | simple | processing | simple |
| SlicerMOOSE | simple | button | simple |
| SlicerMassVision | simple | top level | simple |
| SlicerModalityConverter | simple | button | simple |
| SlicerMorph | simple | top level | simple |
| SlicerMorphoDepot | simple | top level | simple |
| SlicerMultiverSeg | simple | processing | simple |
| SlicerMuscleMap | simple | processing | simple |
| SlicerNNInteractive | simple | top level | display, blocking-prevention, isolated |
| SlicerNNUnet | version | processing | simple |
| SlicerNetstim | simple | processing | simple |
| SlicerNeuro | simple | processing | display |
| SlicerNeuroStrip | simple | top level | simple |
| SlicerNeuropacs | version | processing | simple |
| SlicerOpenLIFU | simple | processing | display |
| SlicerOrbitSurgerySim | simple | top level | display |
| SlicerPhotogrammetry | simple | top level | simple |
| SlicerPipelines | simple | top level | simple |
| SlicerPolycysticKidneySeg | simple | button | simple |
| SlicerPyTorch | simple | button | simple |
| SlicerPythonTestRunner | simple | processing | simple |
| SlicerRVXLiverSegmentation | simple | top level | simple |
| SlicerRadiomics | simple | top level | simple |
| SlicerSPECTRecon | version | top level | display |
| SlicerSandbox | simple | processing | simple |
| SlicerSegmentHumanBody | simple | top level | simple |
| SlicerSegmentWithSAM | simple | top level | simple |
| SlicerSkeletalRepresentation | simple | top level | simple |
| SlicerSoundControl | simple | top level | simple |
| SlicerStereotaxia | simple | processing | simple |
| SlicerSurfaceLearner | simple | top level | simple |
| SlicerThemes | simple | button | simple |
| SlicerTissueSegmentation | simple | top level | simple |
| SlicerTomoSAM | simple | processing | simple |
| SlicerTorchIO | simple | processing | simple |
| SlicerTotalSegmentator | simple | processing | simple |
| SlicerTractParcellation | simple | button | simple |
| SlicerTrame | simple | top level | simple |
| SlicerUltrasound | simple | top level | simple |
| SlicerUniGradICON | simple | top level | simple |
| Slicerflywheelcaseiterator | simple | top level | simple |
| TCIABrowser | simple | top level | simple |
| TOMAAT-Slicer | simple | top level | simple |
| aigt | simple | top level | simple |
| opendose3d | simple | top level | display |
| slicer_flywheel_connect | simple | top level | simple |
In the table above, the green cells are the items I think are worth revisiting and learning from for this project.
Other things I found while looking through these that I’d like to consider:
slicer.util.restart to restart after installslicer.util.showStatusMessageslicer.util.displayPythonShellslicer.util.tryWithErrorDisplayBusyCursor context managerFurther points that I’d like to follow up on:
Using uv instead of pip could provide a huge speedup and unlock many more possibilities. Mike’s AI generated summary provides some inspiration:
uv can be bundled with Slicer. We can add it to the superbuild (but then we are dealing with rust), or we can install it from the wheel. Replacing pip by uv pip is in instant win in terms of speed.uv workspaces can be used to look at python dependencies of a set of Slicer extensions and come up with a single common resolution before actually installing anything. This could be used at Slicer build time on all indexed Slicer extensions. Or it can be used whenever a user is installing a new extension on the set of all their installed extensions.uv’s lock files and ability to roll back to snapshots may solve the problem reverting the Slicer python environment to a clean one while testing each extension.A plan of attack:
uv, conflicts between extensions, reverting the environment for tests.For Slicer extensions, I believe requirements.txt is the right way to specify python dependency requirements.
Why not pyproject.toml?
pyproject.toml one declares a package’s dependencies. This suggests you’re defining a distributable package with a name, version, and build backend. Slicer extensions aren’t python packages. In a Slicer extension we are just specifying “install these things into this environment,” which is exactly what a requirements.txt is.requirements.txt format is pip’s input format. So it removes the need for a translation layer.[project] metadata like name, version, [build-system], etc, which are not relevant.pip install -c constraints.txt is how pip handles dependency conflicts across multiple extensions. Even with pyproject.toml you’d still need a separate constraints.txt file.uv is adopted:
uv supports requirements.txt: uv pip compile requirements.txt -o requirements.lock generates resolved lock filesuv pip compile the lock file that we get is essentially in a requirements.txt format. The TOML uv.lock format is only used with uv lock or uv sync (which are things you’d probably use mainly for managing actual python projects).uv pip compile a.txt b.txt -c constraints.txt without the need for any of the extra pyproject.toml metadataA dependency can be represented by a packaging.requirements.Requirement. A list of such things is what we should get when we load a requirements.txt file (or a constraints.txt file).
pip is using internally to represent requirements.Example of how to load a requirements file:
from packaging.requirements import Requirement
def load_requirements(path):
"""Load requirements.txt into list of Requirement objects."""
reqs = []
with open(path) as f:
for line in f:
line = line.strip()
# Skip comments, empty lines, and pip options (-r, -c, --index-url, etc.)
if line and not line.startswith("#") and not line.startswith("-"):
reqs.append(Requirement(line))
return reqs
One can do a pip --dry-run to use pip’s way of checking, but then we need to call a subprocess which has some overhead.
Unlike installation, dependency checking is an operation that might get called upon frequently.
It would be good to do it in pure python.
It does get a bit complicated mainly because of the possibility of extras in a Requirement,
but it’s not that bad; here is how slicer.util.pip_check might work:
from importlib.metadata import version, requires, PackageNotFoundError
from packaging.requirements import Requirement
from packaging.markers import default_environment
def pip_check(req : Requirement|list[Requirement], _seen=None) -> bool:
"""Check if requirement(s) are satisfied.
For requirements with extras like package[extra1,extra2]>=1.0, this:
1. Checks if the base package is installed at an acceptable version
2. Finds which dependencies are activated by the requested extras
3. Recursively verifies those dependencies are satisfied
Markers (e.g., "; sys_platform == 'win32'") are evaluated - if a marker
doesn't apply to the current environment, the requirement is considered
satisfied (since it doesn't need to be installed).
Args:
req: Either a Requirement object or a list of Requirement objects
_seen: Internal parameter for tracking circular dependencies
Returns:
True if all requirements are satisfied, False otherwise
Example:
from packaging.requirements import Requirement
# Single requirement
req = Requirement("numpy>=1.20")
if pip_check(req):
print("numpy is satisfied")
# Multiple requirements
reqs = [
Requirement("numpy>=1.20"),
Requirement("pandas[excel]>=2.0"),
]
if pip_check(reqs):
print("All requirements satisfied")
"""
if _seen is None:
_seen = set()
# Handle list of requirements, sharing _seen across all of them
if isinstance(req, list):
return all(pip_check(r, _seen) for r in req)
# Check if requirement's marker applies to current environment
# If not, consider it satisfied (doesn't need to be installed here)
if req.marker is not None:
env = default_environment()
if not req.marker.evaluate(env):
return True
# Avoid rechecking the same requirement
key = (req.name.lower(), frozenset(req.extras))
if key in _seen:
return True
_seen.add(key)
# Check if base package is installed at acceptable version
try:
installed = version(req.name)
except PackageNotFoundError:
return False
if installed not in req.specifier:
return False
# If no extras then we are done
if not req.extras:
return True
# Find dependencies activated by the requested extras
dep_strings = requires(req.name) or []
env = default_environment()
activated = []
for dep_str in dep_strings:
dep = Requirement(dep_str)
if dep.marker is None:
continue
# Check if any requested extra activates this dependency
for extra in req.extras:
if dep.marker.evaluate({**env, "extra": extra}):
# Strip marker before recursive check - we've already determined it applies
dep_str_no_marker = str(dep).split(';')[0].strip()
activated.append(Requirement(dep_str_no_marker))
break # Don't check other extras for same dep
# Recursively verify all activated dependencies
return all(pip_check(dep, _seen) for dep in activated)
For the triggering problem I propose an explicit checker function followed by non-top-level imports. Maybe the LazyImportGroup approach can be considered for later, but for now I think something clear and simple is needed. The LazyImportGroup cleverly intercepts your first use of an imported module to trigger installation behind the scenes. It’s elegant, but the magic does reduce transparency for everyday Slicer extension developers, making debugging more difficult. For IDE support and type checking, one can use the TYPE_CHECKING pattern to declare imports at the top of the file, which type checkers see but which doesn’t run at runtime.
Here’s how slicer.util.pip_ensure might work:
from packaging.requirements import Requirement
def pip_ensure(
requirements: list[Requirement],
prompt: bool = True,
requester: str | None = None,
skip_in_testing: bool = True,
show_progress: bool = True,
) -> None:
"""Ensure requirements are satisfied, installing if needed.
Call at the point where dependencies are actually needed (e.g., onApplyButton).
Args:
requirements: List of Requirement objects to check/install
prompt: If True, show confirmation dialog before installing
requester: Name shown in dialog to identify who is requesting the packages
(e.g., "TotalSegmentator", "MyFilter", "console script")
skip_in_testing: If True (default), skip installation when Slicer is running
in testing mode (slicer.app.testingEnabled()). This prevents tests from
modifying the Python environment. Set to False if your test explicitly
needs to verify installation behavior.
show_progress: If True (default), show progress dialog during installation
with status updates and collapsible log details. If False, show only
a busy cursor. Since pip_ensure already shows a confirmation dialog,
showing progress during installation provides a consistent user experience.
Raises:
RuntimeError: If user declines installation or installation fails
Example:
reqs = slicer.util.load_requirements(Path(__file__).parent / "requirements.txt")
slicer.util.pip_ensure(reqs, requester="MyExtension")
import some_package
"""
import logging
missing = [req for req in requirements if not pip_check(req)]
if not missing:
return # all satisfied
# skip installation in testing mode to avoid modifying the environment
if skip_in_testing and slicer.app.testingEnabled():
missing_str = ", ".join(str(req) for req in missing)
logging.info(f"Testing mode is enabled: skipping pip_ensure for [{missing_str}]")
return
if prompt:
package_list = "\n".join(f"• {req}" for req in missing)
title = f"{requester} - Install Python Packages" if requester else "Install Python Packages"
count = len(missing)
message = (
f"{count} Python package{'s' if count != 1 else ''} "
f"need{'s' if count == 1 else ''} to be installed.\n\n"
f"This will modify Slicer's Python environment. Continue?"
)
if not slicer.util.confirmOkCancelDisplay(message, title, detailedText=package_list):
raise RuntimeError("User declined package installation")
# Install missing packages with optional progress display
pip_install_with_progress(
[str(req) for req in missing],
show_progress=show_progress,
requester=requester,
)
The pip_install_with_progress is explained in the Installing section below. Since pip_ensure already shows a confirmation dialog to the user by default, showing progress during the subsequent installation provides a consistent experience as the default.
Say you have a Resources/requirements.txt in your Slicer module and it contains
scikit-image>=0.21
Here’s how you might use pip_ensure to trigger install if needed:
from typing import TYPE_CHECKING
if TYPE_CHECKING:
import skimage
...
class MyFilterWidget(ScriptedLoadableModuleWidget):
...
def onApplyButton(self):
reqs = slicer.util.load_requirements(self.resourcePath("requirements.txt")) # say this contains "scikit-image>=0.21", for example
slicer.util.pip_ensure(reqs, requester="MyFilter")
import skimage
filtered = skimage.filters.gaussian(array, sigma=2.0)
...
We will build upon slicer.util.pip_install to help solve two problems:
Blocking prevention is technical.
We will build this into slicer.util.pip_install by using a QTimer-based polling approach inspired by SlicerMONAIAuto3DSeg.
Progress display will involve creating a modal dialog containing a progress bar and an expandable details section.
I think stuffing progress display functionality into pip_install does not make sense.
pip_install could remain a low-level building block (with the ability to be non-blocking), while pip_install_with_progress could be a high-level utility that always waits for completion while showing a modal dialog. Mixing these in one function would create confusing interactions. For example what should pip_install(blocking=False, show_progress=True) do when the caller expects an immediate return but the progress dialog expects to block until completion? It also feels a bit messy to stuff so much Qt gui code into pip_install itself.
Here is what these two changes end up meaning for Slicer extension developers, if they are succesfully implemented:
pip_install gets optional blocking=False mode with logCallback and completedCallback parameters, allowing advanced users to build custom installation UIs or run pip in the background while keeping the application fully responsive.pip_install_with_progress() function provides an out-of-the-box installation experience with a modal progress dialog showing an animated progress bar, status updates, and a collapsible details panel containing the full pip log. Additionally there would be error handling that displays the complete log if installation fails. For most extension developers, pip_install_with_progress() is the recommended choice for user-facing installations, while the enhanced pip_install() remains available for scripting, automation, or custom UI needs.The above plan was consolidated into a set of instructions for Claude to execute upon.
It ran with --dangerously-skip-permissions inside an isolated docker environment where it had only a Slicer source tree and build tree to play with and test its implementation. When it was done, I requested unit tests following the style and philosophy of existing ones for slicer.util, a self-review based on some similar past pull requests, and useful documentation updates.
Discussions led to a few changes to the design:
pip_install_with_progress into pip_install, because we may actually want to make the modal progress dialog become the default behavior for everyone using pip_install.--no-deps for some dependencies (e.g. to potentially make things like this easier to express)

Prior related work and ideas: