Version managers
Version managers
Different projects need different language runtime versions. Version managers let one machine switch between many versions safely. This article covers the leading tools for Node, Python, and the JVM, plus multi-language managers.
1. Why version managers
Installing language runtimes through the system package manager (apt install nodejs, a Windows MSI installer) usually pins one version. When project A needs Node 18, B needs Node 22, and C needs Node 16, reinstalling each time is impractical. Global system installs also pull in extra problems — global npm packages start asking for admin rights, for example.
What a version manager gives:
- Multiple versions installed in isolation under the user's home directory.
- Auto-switching on shell entry or directory entry (
.nvmrc,.python-version,.tool-versions). - A shim or function that doesn't touch the system PATH.
2. Two operating styles
Most managers fit one of two styles:
- Shim style — Thin wrappers like
~/.pyenv/shims/pythonsit at the front of the PATH. On call, they look at the current directory's.python-version(etc.) and route to the right real binary. Behaves the same in any shell. - Shell function / PATH rewriting — nvm, for example, has a shell function shift Node's spot in the PATH on
nvm use 20. Lightweight but shell-bound.
3. Node.js
| Tool | Released | Trait |
|---|---|---|
| nvm (creationix → nvm-sh) | 2010 | Bash / zsh function. Standard on macOS / Linux. |
| nvm-windows (coreybutler) | 2014 | Written in Go. A Windows port of nvm (a separate project). |
| fnm (Schniz) | 2018 | Rust. Fast shell startup. Cross-platform. |
| Volta (LinkedIn) | 2019 | Rust. Per-project pinning via package.json's "volta" field. |
| Corepack | 2021 (Node 16.10+) | Bundled with Node. Manages package-manager (pnpm / yarn) versions itself. |
4. Python
| Tool | Released | Trait |
|---|---|---|
| pyenv (yyuu) | 2012 | Inspired by rbenv. Shim style. macOS / Linux. |
| pyenv-win | 2018 | Windows port. |
| uv python | 2024 | uv pulls in the Python interpreter itself. |
5. JVM
| Tool | Released | Trait |
|---|---|---|
| SDKMAN! | 2012 | sdkman.io. Java / Kotlin / Gradle / Maven and many JVM tools. macOS / Linux / WSL. |
| jenv | 2014 | Switches between already-installed JDKs on macOS / Linux. |
| jabba | 2017 | Cross-platform. |
6. Multi-language
| Tool | Released | Trait |
|---|---|---|
| asdf (asdf-vm) | 2014 | Plugin-based. Unifies Node, Python, Ruby, Java, etc. |
| mise (jdx) | 2023 | Originally rtx. Rust. asdf-compatible plus env vars and a task runner. |
7. Windows specifics
- Scoop (2013) and winget (Microsoft, 2020) — OS-level package managers. They are good entry points for installing version managers themselves, but they install one version at a time, so multi-version isolation still belongs to the managers above.
- Chocolatey (2011) — Similar territory. Share has been shifting to winget.
8. Common shapes
# nvm (macOS · Linux)
nvm install 20
nvm use 20
echo "20" > .nvmrc
nvm use # auto-applies .nvmrc
# nvm-windows (PowerShell)
nvm install 20.10.0
nvm use 20.10.0
nvm list
# pyenv
pyenv install 3.12.5
pyenv local 3.12.5 # writes .python-version
python --version
# SDKMAN! (macOS · Linux · WSL)
sdk install java 21.0.4-tem
sdk use java 21.0.4-tem
sdk list java
# mise (cross-platform)
mise use node@20 python@3.12 java@21
cat .tool-versions
asdf established the .tool-versions format (one file with multiple tool versions); mise stays compatible.
9. Common pitfalls
System PM + version manager clash — when runtimes installed by the system package manager and by the version manager both sit on the PATH. Check which binary is picked with which python or Get-Command python.
nvm is a bash function — environments that don't go through bash (some IDE-integrated terminals, GUI launchers) hit nvm: command not found. Shim-style fnm or Volta has less friction here.
nvm-windows auto-switching — does not support per-directory auto-switching out of the box (an external helper is needed). fnm handles this.
pyenv builds — compiles from source, so OS headers (build-essential, Xcode CLT, openssl-dev) are required. uv's Python install fetches prebuilt interpreters and dodges this entirely.
SDKMAN! shell rc — adds an init script. Non-interactive shells or CI may need an explicit source $SDKMAN_INIT.
Global npm packages — switching Node versions leaves the new prefix empty, so previously installed tools may seem to vanish.
Closing thoughts
Version managers are nearly required when juggling multiple projects at once. fnm (or Volta) for Node, uv's interpreter management (or pyenv) for Python, SDKMAN! for the JVM, and mise as a single tool for everything come up most. The two real values are auto-switching at shell entry and per-project pinning (.nvmrc, .python-version, .tool-versions).
Next
- git-workflow
- gradle
References include nvm GitHub, nvm-windows GitHub, fnm GitHub, pyenv GitHub, SDKMAN!, asdf docs, mise docs, Volta docs, Corepack docs, Scoop, and winget.