Managing Multiple Python and Java Versions: A Developer’s Guide to pyenv and SDKMAN

Introduction

As developers, we often find ourselves juggling multiple projects, each requiring different versions of Python or Java. Maybe you’re maintaining a legacy application that runs on Python 3.8 while building a new microservice on Python 3.12. Or perhaps you’re working with Java 11 for one client and Java 21 for another. Manually managing these versions can quickly become a nightmare of PATH variables, symlinks, and “it works on my machine” debugging sessions.

This is where version managers come to the rescue. In this guide, we’ll explore two powerful tools that will transform how you handle language versions: pyenv for Python and SDKMAN for Java.

Why Version Managers Matter

Before diving into the tools, let’s understand why version managers are essential:

Isolation: Each project can use its specific language version without conflicts.

Easy switching: Change versions with a single command instead of modifying system configurations.

Reproducibility: Your team can easily replicate the exact development environment.

Safety: Experiment with new versions without risking your system’s stability.

Convenience: Install and manage multiple versions from a single interface.

Part 1: Managing Python Versions with pyenv

pyenv is a simple, powerful tool that lets you easily switch between multiple versions of Python. Unlike system-level installations, pyenv builds Python versions in your home directory, giving you complete control without requiring root access.

Installing pyenv

The installation process varies by operating system, but the most reliable method is using the official installer script.

On macOS and Linux:

curl https://pyenv.run | bash

After installation, add pyenv to your shell configuration. For bash, add these lines to your ~/.bashrc:

export PYENV_ROOT="$HOME/.pyenv"
export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init -)"

For zsh users, add the same lines to ~/.zshrc. If you’re using fish shell, add to ~/.config/fish/config.fish:

set -Ux PYENV_ROOT $HOME/.pyenv
set -U fish_user_paths $PYENV_ROOT/bin $fish_user_paths
pyenv init - | source

Restart your shell or run source ~/.bashrc (or your shell’s config file) to apply the changes.

Essential pyenv Commands

List available Python versions:

pyenv install --list

This shows all Python versions available for installation, including CPython, PyPy, and other implementations.

Install a specific Python version:

pyenv install 3.12.0
pyenv install 3.11.5
pyenv install 3.8.18

pyenv will download, compile, and install the Python version in ~/.pyenv/versions/.

List installed versions:

pyenv versions

The asterisk indicates your currently active version.

Set global Python version:

pyenv global 3.12.0

This sets the default Python version for your entire system (user-level, not system-wide).

Set local Python version for a project:

cd ~/projects/my-legacy-app
pyenv local 3.8.18

This creates a .python-version file in your project directory. Whenever you enter this directory, pyenv automatically switches to that version.

Set Python version for current shell session:

pyenv shell 3.11.5

This temporarily overrides the global and local settings for your current terminal session.

Real-World pyenv Workflow

Here’s how you might use pyenv in practice:

# Install multiple Python versions
pyenv install 3.12.0
pyenv install 3.8.18

# Set Python 3.12 as your default
pyenv global 3.12.0

# Create a new project directory
mkdir ~/projects/legacy-client-app
cd ~/projects/legacy-client-app

# This project needs Python 3.8
pyenv local 3.8.18

# Verify you're using the correct version
python --version  # Output: Python 3.8.18

# Create a virtual environment with this Python version
python -m venv venv
source venv/bin/activate

# Work on your project...
pip install -r requirements.txt

When you leave this directory and enter another project, pyenv automatically switches to that project’s Python version based on its .python-version file.

pyenv with Virtual Environments

pyenv works seamlessly with Python’s built-in venv module and third-party tools like virtualenv. A common pattern is:

  1. Use pyenv to set the Python version for your project
  2. Create a virtual environment using that Python version
  3. Install project dependencies in the virtual environment

This gives you both version isolation (via pyenv) and dependency isolation (via virtual environments).

Troubleshooting Common pyenv Issues

Build dependencies missing: If Python installation fails, you likely need development libraries. On Ubuntu/Debian:

sudo apt-get update
sudo apt-get install -y build-essential libssl-dev zlib1g-dev \
  libbz2-dev libreadline-dev libsqlite3-dev curl \
  libncursesw5-dev xz-utils tk-dev libxml2-dev libxmlsec1-dev libffi-dev liblzma-dev

On macOS, ensure Xcode Command Line Tools are installed:

xcode-select --install

Python version not switching: Make sure pyenv init is in your shell configuration and that you’ve restarted your shell.

Slow Python installation: pyenv compiles Python from source. On macOS, you can use pre-compiled versions with pyenv install --patch:

PYTHON_BUILD_CACHE_PATH=$HOME/.pyenv/cache pyenv install 3.12.0

Part 2: Managing Java Versions with SDKMAN

SDKMAN (Software Development Kit Manager) is a versatile tool for managing parallel versions of multiple Software Development Kits on Unix-based systems. While it supports many SDKs (Gradle, Maven, Kotlin, Scala), we’ll focus on its Java management capabilities.

Installing SDKMAN

Installation is straightforward and works on any Unix-based system:

curl -s "https://get.sdkman.io" | bash

After installation, open a new terminal or run:

source "$HOME/.sdkman/bin/sdkman-init.sh"

Verify the installation:

sdk version

SDKMAN automatically integrates with bash, zsh, and other shells by adding initialization code to your shell’s configuration file.

Essential SDKMAN Commands

List available Java versions:

sdk list java

This displays all available Java distributions, including Oracle JDK, OpenJDK, Amazon Corretto, Temurin (AdoptOpenJDK), GraalVM, and more. You’ll see vendor identifiers and version numbers.

Install a specific Java version:

sdk install java 21.0.1-tem     # Temurin JDK 21
sdk install java 11.0.21-amzn   # Amazon Corretto 11
sdk install java 17.0.9-graal   # GraalVM 17

SDKMAN downloads and installs the JDK in ~/.sdkman/candidates/java/.

List installed Java versions:

sdk list java | grep installed

Or see currently installed versions with:

ls ~/.sdkman/candidates/java/

Set default Java version:

sdk default java 21.0.1-tem

This sets your system-wide default Java version.

Use a specific Java version in current shell:

sdk use java 11.0.21-amzn

This temporarily switches to Java 11 for your current terminal session without changing the default.

Check current Java version:

sdk current java
java -version

Project-Specific Java Versions with .sdkmanrc

SDKMAN supports project-specific SDK versions through a .sdkmanrc file. Create this file in your project root:

cd ~/projects/my-java-app
sdk env init

This creates a .sdkmanrc file. Edit it to specify your Java version:

java=17.0.9-tem

Now, whenever you enter this directory and run:

sdk env

SDKMAN automatically switches to Java 17 for that shell session.

For automatic switching, you can enable the SDKMAN hook in your shell configuration. Add to your ~/.bashrc or ~/.zshrc:

# This loads the sdkman init script and enables auto-env
export SDKMAN_DIR="$HOME/.sdkman"
[[ -s "$HOME/.sdkman/bin/sdkman-init.sh" ]] && source "$HOME/.sdkman/bin/sdkman-init.sh"

# Auto-switch Java version when entering a directory with .sdkmanrc
sdk_auto_env() {
    if [[ -f ".sdkmanrc" ]]; then
        sdk env
    fi
}
cd() {
    builtin cd "$@" && sdk_auto_env
}

For fish shell, first install fisher, then

fisher install edc/bass

Add this to fish config file at ~/.config/fish/config.fish

# SDKMAN initialization
set -gx SDKMAN_DIR $HOME/.sdkman
function sdk
    bass source "$HOME/.sdkman/bin/sdkman-init.sh" ';' sdk $argv
end

Real-World SDKMAN Workflow

Here’s a practical example of using SDKMAN:

# Install multiple Java versions
sdk install java 21.0.1-tem
sdk install java 17.0.9-tem
sdk install java 11.0.21-amzn

# Set Java 21 as default
sdk default java 21.0.1-tem

# Create a project that needs Java 17
mkdir ~/projects/spring-legacy-service
cd ~/projects/spring-legacy-service

# Initialize SDKMAN config for this project
sdk env init

# Edit .sdkmanrc to specify Java 17
echo "java=17.0.9-tem" > .sdkmanrc

# Apply the configuration
sdk env

# Verify you're using Java 17
java -version  # Output: openjdk version "17.0.9"

# Build your project with Maven or Gradle
./gradlew build

When you navigate to different projects, you can quickly switch Java versions:

cd ~/projects/new-microservice
sdk use java 21.0.1-tem  # Use Java 21 for new features

cd ~/projects/legacy-client
sdk use java 11.0.21-amzn  # Switch to Java 11 for compatibility

Managing Other JVM Tools with SDKMAN

SDKMAN isn’t just for Java. It can also manage related tools:

Install Gradle:

sdk install gradle 8.5

Install Maven:

sdk install maven 3.9.6

Install Kotlin:

sdk install kotlin 1.9.21

You can use the same commands (list, install, use, default) for these tools.

Uninstalling Versions

To remove versions you no longer need:

sdk uninstall java 11.0.21-amzn

This helps keep your system clean and saves disk space.

Best Practices and Tips

Commit version files to Git: Both .python-version and .sdkmanrc files should be committed to your repository. This ensures all team members use the same language versions.

Document version requirements: Add a README section explaining which tool versions are required and how to set them up using pyenv and SDKMAN.

Use version ranges carefully: Specify exact versions in production environments but consider more flexible versions for development.

Keep tools updated: Periodically update pyenv and SDKMAN themselves:

# Update pyenv
cd ~/.pyenv && git pull

# SDKMAN updates itself automatically, but you can force it
sdk update

Clean up old versions: Remove unused Python and Java versions to save disk space:

pyenv uninstall 3.7.12
sdk uninstall java 8.0.392-tem

Shell integration matters: Ensure both tools are properly initialized in your shell configuration for the best experience.

Conclusion

Version managers like pyenv and SDKMAN transform the chaos of managing multiple language versions into a streamlined, predictable process. Whether you’re maintaining legacy applications, experimenting with new language features, or simply working across multiple projects, these tools provide the flexibility and control you need.

With pyenv, you can effortlessly switch between Python versions and ensure every project runs on its intended interpreter. With SDKMAN, Java version management becomes trivial, letting you work with different JDKs, distributions, and even other JVM languages without headaches.

The initial setup takes just a few minutes, but the time saved and frustration avoided over the course of your development career is immeasurable. Install these tools today, and never worry about version conflicts again.


Quick Reference Card

pyenv:

  • Install version: pyenv install 3.12.0
  • List versions: pyenv versions
  • Set global: pyenv global 3.12.0
  • Set local: pyenv local 3.11.5
  • Set for shell: pyenv shell 3.10.0

SDKMAN:

  • Install version: sdk install java 21.0.1-tem
  • List versions: sdk list java
  • Set default: sdk default java 21.0.1-tem
  • Use temporarily: sdk use java 17.0.9-tem
  • Check current: sdk current java
  • Project config: Create .sdkmanrc and run sdk env

References: https://github.com/pyenv/pyenv

https://sdkman.io

https://github.com/sdkman/sdkman-cli/issues/671

Leave a comment