Python environments
15 minute read
What you’ll learn
By the end of this tutorial, you’ll be able to:
- Choose the right tool for your Python workflow
- Create reproducible project environments with UV
- Use Pixi for conda-forge packages
- Set up global environments with Micromamba
- Run Python jobs on the cluster
- Troubleshoot common environment issues
Time: About 45 minutes
Prerequisites: Complete Bash Basics and Slurm Basics first.
Why environment management matters
On your laptop, you might install packages globally with pip install. This works until:
- Project A needs
torch 2.0but Project B needstorch 1.13 - You upgrade a package and break an old project
- You can’t reproduce your results because you forgot which versions you used
On DAIC, these problems are amplified:
- Quota limits: Your home directory is only 5 MB
- Shared system: You can’t install packages system-wide
- Reproducibility: Research requires knowing exactly what versions you used
- Collaboration: Others need to run your code with the same dependencies
Environment management tools solve these problems by isolating each project’s dependencies.
The tools
DAIC supports several Python environment tools. Here’s when to use each:
| Tool | Best for | Key feature |
|---|---|---|
| UV | Most projects | Fast, lockfiles, reproducible |
| Pixi | Conda-forge packages | Conda ecosystem, project-based |
| Micromamba | Shared environments | Traditional conda workflow |
| Modules | Pre-installed packages | Zero setup |
This tutorial covers all four, starting with UV (recommended for most users).
Part 1: UV - project-based environments
UV is a fast Python package manager written in Rust. It replaces pip, virtualenv, and pip-tools with a single tool.
Why UV?
- Speed: Parallel downloads and a shared package cache keep installs short
- Lockfiles:
uv.lockrecords exact versions for reproducibility - Project-based: Each project has its own isolated environment
- No activation needed:
uv runhandles everything
Installing UV
Set up your shell first
Complete Shell Setup before installing any of the tools in this tutorial. Without.daicrc, the installers and package caches write to your 5 MB cluster home and fill it.$ curl -LsSf https://astral.sh/uv/install.sh | sh
Restart your shell or run:
$ source ~/.bashrc
Verify the installation:
$ uv --version
uv 0.x.y
Creating a project
Navigate to your project storage and create a new project:
$ cd /tudelft.net/staff-umbrella/<project>
$ uv init --no-package ml-experiment
$ cd ml-experiment
$ ls
README.md main.py pyproject.toml
The --no-package option creates a project that holds scripts rather than an installable Python package, which is what most research code needs.
UV created three files:
pyproject.toml: Project metadata and dependenciesmain.py: A sample Python fileREADME.md: Project documentation
It also initialised a Git repository and added the hidden files .gitignore and .python-version (ls -a shows them).
Look at the project configuration:
$ cat pyproject.toml
[project]
name = "ml-experiment"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12"
dependencies = []
Adding dependencies
Add packages with uv add:
$ uv add torch numpy pandas matplotlib
Resolved 15 packages in 234ms
Installed 15 packages in 1.2s
+ numpy==2.2.1
+ pandas==2.2.3
+ torch==2.5.1
...
UV automatically:
- Creates a virtual environment in
.venv/ - Installs packages
- Updates
pyproject.toml - Generates
uv.lockwith exact versions
Check what was added:
$ cat pyproject.toml
[project]
...
dependencies = [
"matplotlib>=3.10.0",
"numpy>=2.2.1",
"pandas>=2.2.3",
"torch>=2.5.1",
]
The uv.lock file contains exact versions and hashes for reproducibility:
$ head -20 uv.lock
version = 1
revision = 2
requires-python = ">=3.12"
[[package]]
name = "numpy"
version = "2.2.1"
source = { registry = "https://pypi.org/simple" }
...
Running code
Use uv run to execute Python code:
$ uv run python -c "import torch; print(torch.__version__)"
2.5.1
Create a training script:
$ cat > train.py << 'EOF'
import torch
import numpy as np
print(f"PyTorch version: {torch.__version__}")
print(f"NumPy version: {np.__version__}")
print(f"CUDA available: {torch.cuda.is_available()}")
# Simple computation
x = torch.randn(1000, 1000)
y = torch.matmul(x, x.T)
print(f"Matrix multiplication result shape: {y.shape}")
EOF
$ uv run python train.py
PyTorch version: 2.5.1
NumPy version: 2.2.1
CUDA available: False
Matrix multiplication result shape: torch.Size([1000, 1000])
Using UV in Slurm jobs
Create a batch script that uses your UV project:
#!/bin/bash
#SBATCH --account=<your-account>
#SBATCH --partition=all
#SBATCH --time=1:00:00
#SBATCH --cpus-per-task=4
#SBATCH --mem=16G
#SBATCH --gres=gpu:1
#SBATCH --output=train_%j.out
module purge
cd /tudelft.net/staff-umbrella/<project>/ml-experiment
echo "Starting training at $(date)"
srun uv run python train.py
echo "Finished at $(date)"
The script loads no CUDA module. PyTorch packages from PyPI and conda-forge bundle the CUDA runtime libraries they need, so the only requirements are a GPU driver and a GPU model that the build supports (see GPU and CUDA compatibility). The same applies to the Pixi and Micromamba jobs below.
Submit it:
$ sbatch train_job.sh
Submitted batch job 12345
Choosing a PyTorch CUDA build
On Linux, uv add torch installs a CUDA-enabled build from PyPI, so GPU support needs no extra steps. You only need the PyTorch package index when you want a build for a specific CUDA version, for example to reproduce an environment from another system.
Declare the index in pyproject.toml and bind only the PyTorch packages to it:
[[tool.uv.index]]
name = "pytorch-cu128"
url = "https://download.pytorch.org/whl/cu128"
explicit = true
[tool.uv.sources]
torch = { index = "pytorch-cu128" }
torchvision = { index = "pytorch-cu128" }
Then add the packages as usual:
$ uv add torch torchvision
explicit = true restricts the index to the packages listed under [tool.uv.sources]. Without it, UV would also look up every other dependency on the PyTorch index, which carries outdated copies of common packages such as NumPy.
Installing CLI tools
UV can install command-line tools globally (independent of projects):
$ uv tool install ruff
$ uv tool install jupyterlab
$ ruff --version
ruff 0.9.1
$ uv tool list
jupyterlab v4.3.4
ruff v0.9.1
Install jupyterlab, not jupyter: the jupyter package is a metapackage without executables of its own, and uv tool install rejects it.
Syncing on another machine
When you clone a project with UV, restore the exact environment:
$ git clone <repo-url>
$ cd ml-experiment
$ uv sync
Resolved 15 packages in 12ms
Installed 15 packages in 0.8s
The lockfile ensures you get the exact same versions.
Exercise 1: Create a UV project
- Create a new UV project called
data-analysis - Add
pandas,scikit-learn, andmatplotlib - Create a script that loads a sample dataset and prints its shape
- Run it with
uv run
Check your work
$ cd data-analysis
$ ls -A
.git .gitignore .python-version .venv README.md main.py pyproject.toml uv.lock
$ uv run python -c "import sklearn; print(sklearn.__version__)"
1.6.0
The .venv directory and uv.lock file should exist after adding packages.
Part 2: Pixi - When you need conda packages
Pixi is a fast, project-based package manager compatible with conda-forge. Use it when:
- You need packages only available on conda-forge (not PyPI)
- You need non-Python dependencies (CUDA, compilers, system libraries)
- You’re working with conda-based toolchains
Installing Pixi
$ curl -fsSL https://pixi.sh/install.sh | sh
$ source ~/.bashrc
$ pixi --version
pixi 0.40.x
Creating a Pixi project
$ cd /tudelft.net/staff-umbrella/<project>
$ pixi init -c conda-forge -c bioconda bioinformatics-project
$ cd bioinformatics-project
$ ls
pixi.toml
The -c options set the channels the project installs from. Bioconda provides bioinformatics software that conda-forge does not carry; omit it if you do not need it.
Adding packages
Add packages from the configured channels:
$ pixi add python=3.11 numpy pandas
$ pixi add biopython
$ pixi add samtools # command-line tool from bioconda, not available on PyPI
Check the configuration:
$ cat pixi.toml
[workspace]
name = "bioinformatics-project"
channels = ["conda-forge", "bioconda"]
platforms = ["linux-64"]
[dependencies]
python = "3.11.*"
numpy = ">=2.2.1,<3"
pandas = ">=2.2.3,<3"
biopython = ">=1.84,<2"
samtools = ">=1.21,<2"
Running commands
$ pixi run python -c "import Bio; print(Bio.__version__)"
1.84
$ pixi run samtools --version
samtools 1.21
Activating the environment
For interactive work, activate the environment:
$ pixi shell
(bioinformatics-project) $ python
>>> import numpy as np
>>> np.__version__
'2.2.1'
>>> exit()
(bioinformatics-project) $ exit
$
Using Pixi in Slurm jobs
#!/bin/bash
#SBATCH --account=<your-account>
#SBATCH --partition=all
#SBATCH --time=2:00:00
#SBATCH --cpus-per-task=8
#SBATCH --mem=32G
#SBATCH --output=analysis_%j.out
module purge
cd /tudelft.net/staff-umbrella/<project>/bioinformatics-project
srun pixi run python analyze.py
Adding PyPI packages
Pixi can also install from PyPI:
$ pixi add --pypi transformers
Exercise 2: Create a Pixi project
- Create a Pixi project for genomics analysis
- Add
python,biopython, andmatplotlib - Verify biopython is installed with
pixi run python -c "from Bio import SeqIO"
Check your work
$ pixi run python -c "from Bio import SeqIO; print('BioPython loaded')"
BioPython loaded
If you see an import error, check that biopython was added to pixi.toml.
Part 3: Micromamba - Global conda environments
Micromamba is a lightweight, standalone conda implementation. Use it when you need:
- Traditional conda workflows
- Environments shared across multiple projects
- Compatibility with existing conda scripts
Installing Micromamba
$ "${SHELL}" <(curl -L micro.mamba.pm/install.sh)
The installer asks four questions. Keep the binary and the prefix out of the cluster home, and accept the defaults for the other two:
Micromamba binary folder? [~/.local/bin] ~/linuxhome/.local/bin
Init shell (bash)? [Y/n]
Configure conda-forge? [Y/n]
Prefix location? [~/micromamba] ~/linuxhome/.mamba
These match what .daicrc configures: ~/linuxhome/.local/bin is on your PATH, MAMBA_ROOT_PREFIX points to ~/linuxhome/.mamba, and CONDA_ENVS_PATH points to ~/linuxhome/.conda/envs, which is where named environments are stored. micromamba info lists the directories in use.
To keep an environment in project storage instead, for example to share it with your group, create it by path with -p rather than by name:
$ micromamba create -p /tudelft.net/staff-umbrella/<project>/envs/pytorch-env python=3.11
$ micromamba activate /tudelft.net/staff-umbrella/<project>/envs/pytorch-env
Creating environments
$ micromamba create -n pytorch-env python=3.11 pytorch numpy -c conda-forge
$ micromamba activate pytorch-env
(pytorch-env) $ python -c "import torch; print(torch.__version__)"
2.5.1
Managing environments
$ micromamba env list
Name Active Path
pytorch-env * /tudelft.net/staff-homes-linux/n/netid01/.conda/envs/pytorch-env
$ micromamba deactivate
Installing additional packages
$ micromamba activate pytorch-env
(pytorch-env) $ micromamba install pandas scikit-learn -c conda-forge
Using Micromamba in Slurm jobs
#!/bin/bash
#SBATCH --account=<your-account>
#SBATCH --partition=all
#SBATCH --time=4:00:00
#SBATCH --cpus-per-task=4
#SBATCH --mem=16G
#SBATCH --gres=gpu:1
#SBATCH --output=train_%j.out
module purge
# Initialize micromamba for this shell
eval "$(micromamba shell hook --shell bash)"
micromamba activate pytorch-env
cd /tudelft.net/staff-umbrella/<project>/ml-experiment
srun python train.py
Exporting environments
Share your environment with collaborators:
$ micromamba activate pytorch-env
(pytorch-env) $ micromamba env export > environment.yml
Recreate it elsewhere:
$ micromamba create -f environment.yml
Exercise 3: Create a Micromamba environment
- Create an environment called
sci-envwith Python 3.11, numpy, and scipy - Activate it and verify scipy is installed
- Export the environment to
environment.yml
Check your work
$ micromamba activate sci-env
(sci-env) $ python -c "import scipy; print(scipy.__version__)"
1.14.1
$ head -5 environment.yml
name: sci-env
channels:
- conda-forge
dependencies:
- python=3.11.*
Part 4: Using modules for pre-installed packages
DAIC provides pre-installed Python packages through the module system. This is the fastest way to get started if the packages you need are available.
Finding available packages
$ module avail py-
---------------------- /cm/shared/modulefiles/2025/cpu ----------------------
py-numpy/1.26.4 py-scikit-learn/1.5.2 py-pandas/2.2.3
py-torch/2.5.1 py-tensorflow/2.18.0 ...
Loading packages
$ module load 2025/gpu
$ module load py-torch/2.5.1
$ module load py-numpy/1.26.4
$ python -c "import torch; print(torch.__version__)"
2.5.1
Request an A40 or L40 with module PyTorch
Thepy-torch modules contain GPU code for the A40 and L40 only. On an RTX Pro 6000 node torch.cuda.is_available() still returns True, but the first GPU operation fails with CUDA error: no kernel image is available for execution on the device. Request a supported type in jobs that use these modules, for example --gres=gpu:nvidia_l40:1. PyTorch installed from PyPI with UV runs on all DAIC GPU types.Combining modules with virtual environments
Use modules as a base and add extra packages:
$ module load 2025/gpu
$ module load py-torch/2.5.1
$ python -m venv /tudelft.net/staff-umbrella/<project>/venvs/custom-env --system-site-packages
$ source /tudelft.net/staff-umbrella/<project>/venvs/custom-env/bin/activate
(custom-env) $ pip install transformers # adds to module packages
(custom-env) $ python -c "import torch, transformers; print('Both work!')"
Both work!
The --system-site-packages flag gives access to module-installed packages.
Part 5: Real-world ML workflow
Let’s put it all together with a realistic machine learning workflow.
Project structure
ml-project/
├── pyproject.toml # UV project config
├── uv.lock # Locked dependencies
├── src/
│ └── train.py # Training script
├── configs/
│ └── config.yaml # Hyperparameters
├── jobs/
│ └── train.sh # Slurm script
└── outputs/ # Results (gitignored)
Create the project
$ cd /tudelft.net/staff-umbrella/<project>
$ uv init --no-package ml-project
$ cd ml-project
$ mkdir -p src configs jobs outputs
Add dependencies
$ uv add torch torchvision numpy pandas matplotlib pyyaml tqdm
If you need a specific CUDA build of PyTorch, first add the index configuration from Choosing a PyTorch CUDA build to pyproject.toml.
Training script
$ cat > src/train.py << 'EOF'
#!/usr/bin/env python3
"""Simple training script demonstrating UV + Slurm workflow."""
import os
import torch
import torch.nn as nn
from torch.utils.data import DataLoader, TensorDataset
from tqdm import tqdm
def main():
# Check environment
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
print(f"Job ID: {os.environ.get('SLURM_JOB_ID', 'local')}")
print(f"Device: {device}")
if torch.cuda.is_available():
print(f"GPU: {torch.cuda.get_device_name(0)}")
# Simple synthetic data
X = torch.randn(1000, 10)
y = torch.randn(1000, 1)
dataset = TensorDataset(X, y)
loader = DataLoader(dataset, batch_size=32, shuffle=True)
# Simple model
model = nn.Sequential(
nn.Linear(10, 64),
nn.ReLU(),
nn.Linear(64, 1)
).to(device)
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
criterion = nn.MSELoss()
# Training loop
epochs = 10
for epoch in range(epochs):
total_loss = 0
for batch_X, batch_y in tqdm(loader, desc=f"Epoch {epoch+1}/{epochs}"):
batch_X, batch_y = batch_X.to(device), batch_y.to(device)
optimizer.zero_grad()
pred = model(batch_X)
loss = criterion(pred, batch_y)
loss.backward()
optimizer.step()
total_loss += loss.item()
print(f"Epoch {epoch+1}, Loss: {total_loss/len(loader):.4f}")
# Save model
os.makedirs('outputs', exist_ok=True)
torch.save(model.state_dict(), 'outputs/model.pt')
print("Model saved to outputs/model.pt")
if __name__ == '__main__':
main()
EOF
Slurm job script
$ cat > jobs/train.sh << 'EOF'
#!/bin/bash
#SBATCH --account=<your-account>
#SBATCH --partition=all
#SBATCH --time=1:00:00
#SBATCH --cpus-per-task=4
#SBATCH --mem=16G
#SBATCH --gres=gpu:1
#SBATCH --output=outputs/train_%j.out
#SBATCH --error=outputs/train_%j.err
# Clean environment
module purge
# Navigate to project
cd /tudelft.net/staff-umbrella/<project>/ml-project
echo "=========================================="
echo "Job started: $(date)"
echo "Job ID: $SLURM_JOB_ID"
echo "Node: $(hostname)"
echo "=========================================="
# Run training
srun uv run python src/train.py
echo "=========================================="
echo "Job finished: $(date)"
echo "=========================================="
EOF
Test interactively, then submit
The login node is not meant for computation, so run the quick test in a short interactive allocation (see Slurm Basics). Submit from the project directory: the --output path in the job script is relative to the directory you submit from, and the job fails without any output if outputs/ does not exist there.
# Quick test on a compute node
$ salloc --account=<your-account> --partition=all --time=0:15:00 --cpus-per-task=4 --mem=8G --gres=gpu:1
$ srun uv run python src/train.py
$ exit
# Submit the full job
$ sbatch jobs/train.sh
Submitted batch job 12345
# Monitor
$ squeue -u $USER
$ tail -f outputs/train_12345.out
Exercise 4: Complete ML workflow
- Create the project structure above
- Modify the training script to save loss history to a CSV file
- Submit a job and verify the output files are created
Check your work
After the job completes:
$ ls outputs/
model.pt train_12345.out train_12345.err
$ tail -5 outputs/train_12345.out
Epoch 10, Loss: 0.9823
Model saved to outputs/model.pt
==========================================
Job finished: Fri Mar 20 15:30:00 CET 2026
==========================================
Troubleshooting
“Disk quota exceeded”
Your cluster home is full (5 MB limit), usually because a tool wrote its cache there.
Solution: Install .daicrc as described in Shell Setup; it redirects the caches of UV, Pixi, conda, pip and others to ~/linuxhome. To free a home that is already full, see Redirecting caches out of the cluster home.
“Failed to hardlink files; falling back to full copy”
UV links packages from its cache into .venv/. The cache is in ~/linuxhome and your project is in project storage; these are different filesystems, so linking is not possible and UV copies the files instead. The warning is harmless. Silence it by adding this to ~/.bashrc:
export UV_LINK_MODE=copy
“Module not found” in Slurm job
The package works locally but fails in the job.
Causes:
- Forgot to use
uv runor activate environment - Different working directory
- Missing module load
Solution: Always use absolute paths and uv run:
cd /tudelft.net/staff-umbrella/<project>/ml-project
srun uv run python src/train.py
GPU and CUDA compatibility
A PyTorch build has to fit the node in two ways: the GPU driver must support the CUDA version the build was compiled for, and the build must contain code for the GPU model.
“no kernel image is available for execution on the device”: the build has no code for this GPU model. torch.cuda.is_available() returns True and the error appears at the first GPU operation. On DAIC this happens on RTX Pro 6000 (Blackwell) nodes with the py-torch modules and with any PyTorch build for a CUDA version older than 12.8. Use a current build from PyPI (uv add torch) or the cu128 index, or request an A40 or L40 with --gres=gpu:nvidia_l40:1.
torch.cuda.is_available() returns False on a GPU node, or PyTorch reports that the driver is too old: the build was compiled for a newer CUDA version than the driver supports. A CUDA module does not change this, because PyTorch uses its bundled CUDA runtime. Check the driver on a compute node; the login node has no GPU, so run this inside a GPU job or interactive session:
$ srun nvidia-smi | grep "CUDA"
| NVIDIA-SMI 610.43.02 KMD Version: 610.43.02 CUDA UMD Version: 13.3 |
The CUDA version shown is the newest CUDA runtime the driver supports. Select a PyTorch build for that version or an older one, as described in Choosing a PyTorch CUDA build.
Slow package installation
Package resolution takes forever.
Cause: Network issues or PyPI server problems.
Solution: UV and Pixi are faster than pip/conda. If still slow, try:
$ uv add package --no-cache # Skip cache if corrupted
Environment not reproducible
Different results on different machines.
Solution: Always commit lockfiles:
$ git add uv.lock pyproject.toml # For UV
$ git add pixi.lock pixi.toml # For Pixi
Exercise 5: Restore from lockfile
- Create a UV project and add packages
- Delete
.venv/to simulate a fresh clone - Run
uv syncto restore the exact environment - Verify packages work
Check your work
$ rm -rf .venv/
$ ls
pyproject.toml uv.lock src/
$ uv sync
Resolved 15 packages in 12ms
Installed 15 packages in 0.8s
$ uv run python -c "import torch; print('Restored!')"
Restored!
The lockfile (uv.lock) ensures the exact same versions are installed.
Summary
You’ve learned to manage Python environments on DAIC:
| Tool | When to use | Key commands |
|---|---|---|
| UV | Most projects | uv init, uv add, uv run |
| Pixi | Conda-forge packages | pixi init, pixi add, pixi run |
| Micromamba | Global environments | micromamba create, micromamba activate |
| Modules | Pre-installed packages | module load py-torch/2.5.1 |
Key takeaways
- Use UV for most projects - it’s fast and handles lockfiles automatically
- Store everything in project storage - never in
/home(5 MB limit) - Commit lockfiles -
uv.lockorpixi.lockfor reproducibility - Test in a short interactive session before submitting - catch errors early
- Match the PyTorch build to the node - its CUDA version must not be newer than the one
nvidia-smireports, and RTX Pro 6000 GPUs need a build for CUDA 12.8 or newer
Quick reference
# UV workflow
$ uv init --no-package myproject && cd myproject
$ uv add torch numpy pandas
$ uv run python train.py
# Pixi workflow
$ pixi init myproject && cd myproject
$ pixi add python pytorch numpy
$ pixi run python train.py
# Micromamba workflow
$ micromamba create -n myenv python=3.11 pytorch -c conda-forge
$ micromamba activate myenv
$ python train.py
Next steps
- Apptainer Tutorial - Package entire environments in containers
- Slurm Tutorial - Advanced job scheduling
- Python Setup Quickstart - Quick reference