It's minimal, but I'm posting things.
Solution: Combining a version manager such as asdf with an environment variable manager such as direnv.
I use asdf1 for runtime management.
It handles my Python versions through a simple .tool-versions file.
direnv2 loads environment variables when I enter a directory and wipes them when I leave.
For Python, I migrated my entire workflow to uv3. It is fast enough that I can sync dependencies every time I switch projects without noticing the latency.
I implemented a script called direnv-provision-python.sh (see end of article for implementation), and added it to my $PATH.
It parses pyproject.toml, extracts the exact version, and generates the necessary local configuration.
It also injects my personal tools, like pdbpp which is a Python debugger library I use when working in Vim.
layout python3 .venv
uv sync --active --preview-features extra-build-dependencies
uv pip install --quiet pdbpp bpython
So I get my debugger and my AI tools. My teammates get a clean project file.
The last piece of the puzzle is Git hygiene.
I don't want to be put in a situation where I must explain why .envrc or .tool-versions are showing up in the repository for my team.
So, what you can do, is specify in your global git configuration an exclusion file, in which you add glob exclusion patterns (see end of article for those).
[core]
excludesfile = /home/dehi/.ignore-git
My ~/.ignore-git file hides my local artifacts from every repository on my machine. It keeps my workflow personal and my commits professional.
Now, you'll enjoy automatic runtime version selecting, environment variable provisionning, and all without polluting your collegues workspace.
direnv-provision-python.sh#! /usr/bin/zsh
direnv-provision-python() {
# Helper to provision a Python project with direnv.
if [[ ! -d "$(pwd)" ]]; then
echo "Error: Current directory does not exist or is inaccessible" >&2
return 1
fi
if [[ ! -w "$(pwd)" ]]; then
echo "Error: Current directory is not writable" >&2
return 1
fi
# Extract Python version from pyproject.toml and create .tool-versions
if [[ -f "pyproject.toml" ]]; then
local python_version
python_version=$(python3 -c "
import sys
try:
import tomllib
except ImportError:
import tomli as tomllib
try:
with open('pyproject.toml', 'rb') as f:
data = tomllib.load(f)
requires = data.get('project', {}).get('requires-python', '')
if requires and '==' in requires:
print(requires.split('==')[1].strip())
else:
print('', file=sys.stderr)
sys.exit(1)
except Exception as e:
print(f'Error parsing pyproject.toml: {e}', file=sys.stderr)
sys.exit(1)
" 2>/dev/null)
if [[ -n "$python_version" ]]; then
echo "python $python_version" > .tool-versions
echo "Created .tool-versions with python $python_version"
else
echo "Warning: Could not extract Python version from pyproject.toml" >&2
fi
fi
local start_marker="### DIR_ENV_PROVISION_PYTHON_START ###"
local end_marker="### DIR_ENV_PROVISION_PYTHON_END ###"
local deps_assignment
if (( $# )); then
deps_assignment="required_deps=("
local dep
for dep in "$@"; do
deps_assignment+="\"${dep//\"/\\\"}\" "
done
deps_assignment="${deps_assignment% }"
deps_assignment+=")"
else
deps_assignment="required_deps=()"
fi
local content
content=$(
{
cat <<'EOF'
PATH_add "$HOME/.asdf/shims"
layout python3 .venv
uv sync --active --preview-features extra-build-dependencies
uv pip install --quiet pdbpp bpython
EOF
}
)
local desired_block="$start_marker
$content
$end_marker"
# Ensure .envrc exists before any operations
[[ -f .envrc ]] || touch .envrc
local existing_block=""
existing_block="$(sed -n "/$start_marker/,/$end_marker/p" .envrc)"
if [[ "$existing_block" != "$desired_block" ]]; then
sed -i.bak "/$start_marker/,/$end_marker/d" .envrc
{
printf "%s\n" "$start_marker"
printf "%s\n" "$content"
printf "%s\n" "$end_marker"
} >> .envrc
fi
}
direnv-provision-python "$@"
**/.tool-versions
*.SAFE
*.class
*.egg-info
*.orig
*.porig
*.pyc
*.rej
*.swp
*.un~
*_BACKUP_*
*_BASE_*
*_LOCAL_*
*_REMOTE_*
.akku
.basedpyright
.clj-kondo
.direnv
.direnv/python-*
.envrc
.envrc.bak
.git
.git_worktree_hooks
.gitconfig
.idea
.lsp
.npm
.opencode
.ropeproject
.tags
.venv
.vscode
/tmp
AGENTS.md
PROJECT.md
Session.vim
__pycache__
bun.lock*
generated
node_modules
nohup.out
openspec
package-lock.json
package.json
profile.log
scheme-docs/resources
session-ses_*.md
site-packages
tags
tags.tmp
target
uv.lock
venv
vilog
xaa
~/.secrets
~/.vim
~/.vim/plugged
~/.win/
~/Games
~/tmp/.vim