Taming the Python Hydra: A Modern Dev Environment with uv

Python development. Powerful, expressive, versatile… and sometimes, utterly chaotic when it comes to managing projects. If you’ve been developing in Python for any length of time, you’ve likely wrestled the multi-headed hydra: juggling different Python versions for different projects, untangling dependency conflicts, ensuring environments are reproducible, and just generally spending too much time fighting your tools instead of writing code.
The Python ecosystem, in its vibrant, sprawling way, has offered many solutions over the years. We’ve had pyenv for managing Python versions, venv or virtualenv
for environment isolation, pip for package installation (often paired with requirements.txt
), and more comprehensive tools like Poetry and PDM aiming to manage the whole project lifecycle. Each solved parts of the puzzle, but often led to a fragmented toolbox – maybe you used pyenv
+ Poetry
, or venv
+ pip-tools
. It worked, but was it fast? Was it simple?
For me the answer was increasingly “no.” Setup felt complex, dependency resolution could be slow, and switching between projects required conscious effort to manage environments. I started searching for a better way, a more unified approach that could harness the speed of modern hardware and streamline the entire workflow.
That search led me to uv.
This post is the story of that journey. We’ll explore why the Python environment hydra is so tricky, why uv
feels like the sharpest sword to tame it in 2025, and how you can set up a development workflow inspired by my own, leveraging uv
as a standalone tool for remarkable speed and simplicity.
The Hydra’s Heads: Why Is Python Management So Tricky?
Before we crown a new champion, let’s appreciate the challenges it needs to conquer:
- Multiple Python Versions: Project A needs Python 3.9 for legacy reasons, Project B uses shiny new features in 3.12, and Project C needs testing across both. Installing Python globally is asking for trouble. Tools like
pyenv
solved this but required shell configuration and manual switching. - Dependency Conflicts (“Dependency Hell”): Project A needs
libraryX v1.0
, but Project B requireslibraryX v2.0
. Or worse, Project A depends onlibraryY
which needslibraryX v1.0
, while Project B depends directly onlibraryX v2.0
. Managing these conflicts within isolated environments is key. - Reproducibility: How do you ensure your colleague (or your future self, or the production server) can recreate the exact environment with the exact versions of all dependencies (including dependencies of dependencies)? Basic
pip freeze > requirements.txt
often isn’t robust enough. Lockfiles are the answer, pioneered by tools like Poetry and pip-tools. - Performance: Waiting minutes for dependencies to resolve or install is a major productivity killer. Traditional tools, often written in Python themselves, can struggle with complex dependency graphs or large numbers of packages. This is where newer, performance-focused tools show their strength.
- Tooling Overload:
pyenv
for versions,venv
for environments,pip
for installing,pip-tools
for locking,pipx
for installing CLI tools… The sheer number of tools needed for a “complete” setup adds cognitive overhead and setup friction.
Enter uv: A Faster, Unified Challenger
Developed by Astral (the same folks behind the incredibly fast linter/formatter Ruff), uv
is an extremely fast Python package installer and resolver, written in Rust. But its ambitions go far beyond just replacing pip
.
uv
aims to be a unified tool, capable of replacing:
pip
(package installation)pip-tools
(dependency locking viauv lock
,uv sync
)venv
/virtualenv
(environment creation viauv venv
)pyenv
(Python version management viauv python install
,uv python pin
)pipx
(installing and running CLI tools globally viauv tool install
,uvx
)
Why is this compelling?
- Blazing Speed: Seriously, it’s fast. Dependency resolution and installation can be 10-100x faster than
pip
or Poetry. On an M-series Mac, this feels like unleashing the hardware’s potential. - Simplicity: One tool to install, learn, and manage. Less configuration, fewer moving parts.
- Standards-Based: It works seamlessly with
pyproject.toml
(using the standard[project]
table defined in PEP 621) andrequirements.txt
files. It generates universal lockfiles (uv.lock
). - Modern Features: Built-in Python version management, dependency groups, environment management, script running (
uv run
), and global tool installation.
While newer than some established tools, uv
is maturing incredibly quickly, backed by significant resources and gaining massive community traction. As of April 2025, it feels like the most promising path towards a streamlined, high-performance Python development future.
Crafting the Ideal Workflow: What Does “Good” Feel Like?
My goal wasn’t just to use a new tool, but to create a workflow that felt effortless. Based on my exploration and implementation (which uses zsh
and Homebrew on macOS, managed with chezmoi), the ideal workflow enabled by uv
should provide:
- Automatic Python Version Switching:
cd
into a project, and the correct Python version (project-specific or a global default) is instantly active without needingsource .venv/bin/activate
. - Easy Global Python Management: A simple way to install Python versions and set a global default, perhaps even keeping that default automatically updated to the latest stable release.
- Seamless Project Management: Creating new projects, adding dependencies, locking, and syncing environments should be quick, standard commands.
- Effortless CLI Tools: Installing and updating global command-line tools written in Python (like
ruff
,black
,httpie
, or custom scripts) should be trivial.
Let’s build this.
The Implementation Guide: Setting Up Your uv Environment
These steps will guide you through setting up uv
as your primary Python environment manager, inspired by my own setup but generalized for broader use. We’ll primarily use zsh
examples for shell integration, but provide pointers for others.
Prerequisites:
- A terminal (like macOS Terminal, iTerm2, WezTerm).
- On macOS: Xcode Command Line Tools. Install via
xcode-select --install
. (uv may need these to build Python or packages). - On Linux: Ensure you have necessary build tools (like
gcc
,make
,libssl-dev
, etc.). Consult your distribution’s documentation or the Python Developer’s Guide.
Step 1: Install uv
The recommended way is the official script:
curl -LsSf https://astral.sh/uv/install.sh | sh
Alternatively, use a package manager:
- macOS (Homebrew):
brew install uv
- Linux/Other: Check the official uv installation guide for more methods (like
pipx
,cargo
).
After installation, close and reopen your terminal or source your shell profile (source ~/.zshrc
, source ~/.bashrc
, etc.). Verify the installation:
uv --version# Should output the installed uv version
Step 2: Install Python Versions
Use uv
to install the Python versions you need. It automatically fetches native ARM64 builds on Apple Silicon.
# Install specific versionsuv python install 3.11 3.12
# List installed versionsuv python list
# See where uv installs Pythons (optional)uv python dir
You can optionally set a global default Python version that uv
will use when no project-specific version is set:
uv python pin --global 3.12
Step 3: Automate Global Python Updates (Optional, but Nice!)
Keeping your global default Python fresh requires occasional updates. We can automate checking for the latest stable release and updating if needed.
Here’s a script that finds the latest stable cpython
version available via uv
, compares it to your current global pin, and installs/pins the latest if it’s newer:
#!/bin/sh
# Script to check and update the globally pinned uv Python version to latest stable
echo "Checking global Python version..."
# Find the latest stable CPython version (X.Y.Z format) available via uv# Filters for cpython-X.Y.Z-<arch>-<os>, extracts X.Y.Z, sorts, gets latest# Use --all-platforms to ensure visibility even if only different arch/os is installedLATEST_PYTHON=$(uv python list --all-platforms | grep '<download available>' | grep '^cpython-[0-9]\+\.[0-9]\+\.[0-9]\+-' | sed -n -E 's/^cpython-([0-9]+\.[0-9]+\.[0-9]+).*/\1/p' | sort -V | tail -n 1)
if [ -z "$LATEST_PYTHON" ]; then echo "Error: Could not determine the latest stable Python version from 'uv python list'." >&2 # It's safer to exit non-zero if we can't determine the latest version exit 1fiecho "Latest stable Python available: $LATEST_PYTHON"
# Get the currently pinned global Python version (ignore errors if none is pinned)CURRENT_PYTHON=$(uv python pin --global 2>/dev/null || echo "")
echo "Currently pinned global Python: ${CURRENT_PYTHON:-'None'}"
# Compare and update if necessaryif [ "$CURRENT_PYTHON" != "$LATEST_PYTHON" ]; then echo "Updating global Python from '${CURRENT_PYTHON:-'None'}' to '$LATEST_PYTHON'..." # Attempt to install and pin; exit non-zero on failure if uv python install "$LATEST_PYTHON" && uv python pin --global "$LATEST_PYTHON"; then echo "Successfully updated and pinned Python $LATEST_PYTHON." else echo "Error: Failed to install or pin Python $LATEST_PYTHON." >&2 exit 1 fielse echo "Global Python ($CURRENT_PYTHON) is already the latest stable version."fi
exit 0
How to run this script?
- Manually: Save it as
update_global_python.sh
, make it executable (chmod +x ./update_global_python.sh
), and run it periodically (./update_global_python.sh
). - Cron Job: Schedule it to run daily/weekly using
crontab -e
. - Automation Tools: If you use tools like
chezmoi
(like I do), you can configure it to run automatically (e.g., using arun_onchange_
script triggered by changes to the script itself, or arun_always_
script).
Step 4: Integrate with Your Shell (Automatic Switching Magic!)
This is where we achieve the seamless cd
-based environment switching, eliminating the need for source .venv/bin/activate
. The core idea is to use a shell hook that runs every time you change directories (chpwd
in zsh). This hook checks for a uv
-managed Python (project-specific first, then global) and updates your PATH
accordingly.
For zsh users:
-
Add this function to your
~/.zshrc
(or a file sourced by it):~/.zshrc _update_uv_python_path() {# Check if uv command existscommand -v uv &>/dev/null || return 0local uv_python_path=""local uv_python_bin_dir=""local uv_root_dir=""local target_version=""# 1. Check for project-specific Python (.python-version file)# Use 'uv python find --quiet .' which respects .python-version# Redirect stderr to /dev/null to suppress "No project found" messagesuv_python_path=$(uv python find --quiet . 2>/dev/null)# 2. If no project-specific Python, check for globally pinned Pythonif [[ -z "$uv_python_path" ]]; then# Get the globally pinned version string (e.g., "3.12")target_version=$(uv python pin --global 2>/dev/null)if [[ -n "$target_version" ]]; then# Find the actual path for that pinned versionuv_python_path=$(uv python find --quiet "$target_version" 2>/dev/null)fifi# 3. If still no uv-managed Python found for the context, potentially clear old paths and exitif [[ -z "$uv_python_path" ]]; then# Optional: Clean up any previous uv python paths if desired# uv_root_dir=$(uv python dir 2>/dev/null || echo "")# if [[ -n "$uv_root_dir" ]]; then# PATH=$(echo "$PATH" | awk -v RS=: -v ORS=: -v uv_root="$uv_root_dir" 'index($0, uv_root) != 1 {print}' | sed 's|:*$||')# fireturn 0fi# 4. Get the bin directory of the found Pythonuv_python_bin_dir=$(dirname "$uv_python_path")# 5. If the correct bin dir is already first in PATH, do nothing[[ "$PATH" == "$uv_python_bin_dir:"* ]] && return 0# 6. Clean up old uv Python paths from PATH# Get the root directory where uv installs Pythonsuv_root_dir=$(uv python dir 2>/dev/null || echo "")if [[ -n "$uv_root_dir" ]]; then# Escape potential special characters in path for awk/sed if necessary# Using a simple prefix check should be safe for typical paths.PATH=$(echo "$PATH" | awk -v RS=: -v ORS=: -v uv_root="$uv_root_dir" 'index($0, uv_root) != 1 {print}' | sed 's|:*$||')fi# 7. Prepend the correct Python bin directory to PATHexport PATH="$uv_python_bin_dir:$PATH"}# Load the hook system and register the function to run on directory changeautoload -Uz add-zsh-hookadd-zsh-hook chpwd _update_uv_python_path# Run the function once immediately on shell startup_update_uv_python_path -
Aliases (Optional but recommended): Add these aliases to your
~/.zshrc
to ensurepython
andpip
commands run within the context managed byuv
when inside a project directory (it uses the virtual environment automatically):~/.zshrc # Run python/pip via uv run if in a uv project context# Note: `uv run` automatically handles the virtualenvalias python='uv run python "$@"'alias python3='uv run python3 "$@"'# You might alias pip too, though using `uv add/remove/sync` is preferred# alias pip='uv run pip "$@"' -
Restart your shell or run
source ~/.zshrc
.
For Bash/Fish users:
- Bash: You’ll need a different mechanism, often involving manipulating the
PROMPT_COMMAND
variable to run a function before the prompt is displayed. See the Bash documentation or community examples forpyenv/nodenv
. - Fish: Uses functions triggered by changes to the
PWD
variable (e.g.,function --on-variable PWD my_hook
). See the Fish documentation on event handlers.
Consult the documentation for your specific shell. The uv documentation or community discussions might also provide guidance or ready-made snippets.
Step 5: Managing Your Projects
Now, the day-to-day workflow becomes much smoother:
-
Create a new project:
Terminal window mkdir my_new_projectcd my_new_project -
Pin a Python version for this project (Optional): If you don’t want the global default. This creates a
.python-version
file.Terminal window uv python pin 3.11# Now, the shell hook (if active) will automatically use 3.11 here -
Initialize the project: Creates
pyproject.toml
and potentially.venv
.Terminal window uv init# Follow prompts for name, version, etc. -
Add dependencies: Updates
pyproject.toml
and installs into the implicitly managed.venv
.Terminal window # Add main dependenciesuv add requests "flask>=2.0"# Add development dependenciesuv add --dev pytest ruff black -
Lock dependencies: Creates
uv.lock
with exact versions of everything. Commitpyproject.toml
anduv.lock
to Git.Terminal window uv lock -
Sync environment: Installs exact versions from
uv.lock
. Use this after cloning or pulling changes.Terminal window uv sync# Use `uv sync --strict` (or similar flag, check `uv sync --help`)# to ensure the environment exactly matches the lockfile (removes extra packages) -
Run code/commands: Executes within the project’s virtual environment without manual activation, thanks to the shell hook or the
uv run
command (or our alias).Terminal window # If using the alias:python src/my_app/main.pypytest# Explicitly using uv run (always works, even without the hook/alias):uv run python src/my_app/main.pyuv run pytestuv run flask --app src/my_app:app run --debug -
Update dependencies:
Terminal window # Update specific package(s) in the lockfile to latest compatibleuv lock --upgrade requests flask# Update all packages in the lockfileuv lock --upgrade# After updating the lockfile, sync the environmentuv sync
Step 6: Managing Global CLI Tools
uv
can replace pipx
for installing Python-based command-line tools globally.
-
Install a tool:
Terminal window uv tool install ruffuv tool install blackuv tool install httpie -
Run an installed tool: Just type its name.
Terminal window ruff check .black .http --help -
List installed tools:
Terminal window uv tool list -
Uninstall a tool:
Terminal window uv tool uninstall ruff -
Update tools:
Terminal window # Update a specific tooluv tool install --upgrade ruff# Update all tools (requires scripting, see below) -
PATH Configuration: Tools are installed in a central location (
~/.local/bin
is common, checkuv tool dir --show-bin
).uv
’s initial install script (curl ... | sh
) usually handles this. If commands aren’t found after installinguv
or tools, ensure theuv
bin directory (often~/.local/bin
) is in yourPATH
. Your shell profile (~/.zshenv
,~/.zshrc
,~/.bash_profile
,~/.profile
,~/.config/fish/config.fish
) might need a line like:Terminal window export PATH="$HOME/.local/bin:$PATH" # Adjust path if neededThen restart your shell.
-
Automate Tool Updates (Optional): You can create a simple script to keep a list of desired global tools up-to-date.
update_global_tools.sh #!/bin/sh# Script to install/update a list of global Python tools using uv# Define tools in a multi-line string, one tool per lineTOOLS="ruffblackhttpiegitingest# Add other desired tools here, ensuring no trailing spaces"echo "Updating global Python tools via uv..."# Use `echo` and `while read` to iterate over lines safelyecho "$TOOLS" | while IFS= read -r tool || [ -n "$tool" ]; do# Skip empty lines or lines starting with #case "$tool" in''|\#*) continue ;;esacecho "Ensuring $tool is installed and up-to-date..."# Use -U as a shorthand for --upgrade# Add error checkingif uv tool install -U "$tool"; then: # Success, do nothingelseecho "Warning: Failed to install/update $tool." >&2# Decide if you want to exit or continue# exit 1 # Exit script on first failure# Or just continue with the next tool (current behavior)fidoneecho "Global tool update process finished."exit 0Save this (e.g.,
update_global_tools.sh
), make it executable (chmod +x ./update_global_tools.sh
), and run it manually (./update_global_tools.sh
), via cron, or through automation likechezmoi
.
Switching Tracks: Migrating Your Existing Projects to uv
Adopting uv
for new projects is straightforward, but what about your existing codebase? Migrating from established setups like requirements.txt
or Poetry is definitely feasible, and uv
provides commands to help smooth the transition. Let’s look at the common scenarios.
Scenario 1: Migrating from requirements.txt
(often with venv
+ pip
/pip-tools
)
This is perhaps the most common setup for many existing applications. You likely have a requirements.txt
file (maybe generated by pip freeze
or pip-compile
) and potentially separate files like requirements-dev.txt
.
-
Navigate to your project directory:
Terminal window cd path/to/your/existing_project -
Pin the Python Version (Recommended): Ensure
uv
knows which Python version this project needs. If you don’t have a.python-version
file yet, create one:Terminal window # Replace <version> with the actual version, e.g., 3.10uv python pin <version>Make sure you have this version installed via
uv python install <version>
if needed. -
Initialize
uv
: This creates thepyproject.toml
file if it doesn’t exist.uv
will recognize existing virtual environments (like.venv
) or create one.Terminal window uv init# Answer the prompts for project name, etc., or edit pyproject.toml later -
Add Dependencies from Requirements Files: Use
uv add -r
to import dependencies directly from your existing files intopyproject.toml
.Terminal window # Add main dependenciesuv add -r requirements.txt# Add development dependencies (if you have a separate file)# Make sure the --dev group matches your needs or adjust as necessaryuv add -r requirements-dev.txt --devReview your
pyproject.toml
to ensure the dependencies under[project.dependencies]
and[tool.uv.dev-dependencies]
(or other groups you might define) look correct. -
Generate the
uv
Lockfile: Create the definitiveuv.lock
based on the dependencies now listed inpyproject.toml
.Terminal window uv lock -
Sync Your Environment: Install everything specified in the new
uv.lock
into your virtual environment (.venv
).Terminal window uv sync# Use --strict if desired to remove packages not in the lockfile -
Cleanup: Once you’ve verified everything works (run your tests!), you can safely:
- Delete the old
requirements.txt
andrequirements-dev.txt
files. - Commit the new
pyproject.toml
anduv.lock
to version control. - Crucially: Update any CI/CD pipelines, Dockerfiles, or deployment scripts. Replace commands like
pip install -r requirements.txt
withuv sync
.
- Delete the old
Scenario 2: Migrating from Poetry
Poetry also uses pyproject.toml
but has its own [tool.poetry]
section and poetry.lock
file. uv
uses the standard [project]
section and its own lockfile format.
-
Navigate to your project directory:
Terminal window cd path/to/your/poetry_project -
Pin the Python Version: Just like before, ensure
uv
knows the target Python version:Terminal window uv python pin <version> # e.g., 3.11 -
Convert
pyproject.toml
: This is the most manual step.uv
doesn’t automatically convert Poetry’s specific format. You need to translate the metadata and dependencies from the[tool.poetry]
section to the standard[project]
and[tool.uv.dev-dependencies]
sections.- Project Metadata: Copy fields like
name
,version
,description
,authors
,license
,readme
from[tool.poetry]
into the corresponding fields under[project]
(PEP 621 standard). - Main Dependencies: Move dependencies listed under
[tool.poetry.dependencies]
to[project.dependencies]
. - Development Dependencies: Move dependencies from
[tool.poetry.group.dev.dependencies]
(or similar Poetry groups) to[tool.uv.dev-dependencies]
. If you have other groups, map them similarly under[tool.uv.tool.<group_name>.dependencies]
. - Build System: Check the
[build-system]
table. Poetry usespoetry-core
. You can keep this if you still want to use Poetry’s build capabilities alongsideuv
for environment management, or switch to another backend likehatchling
orsetuptools
if you plan to useuv build
(which invokes the specified backend). - Remove Poetry Section: Once everything is migrated, delete the entire
[tool.poetry]
section frompyproject.toml
.
Example
pyproject.toml
Conversion:Let’s say your original
pyproject.toml
looked something like this (simplified):pyproject.toml # Original Poetry pyproject.toml (Before Migration)[tool.poetry]name = "my-awesome-app"version = "1.2.3"description = "Does awesome things."authors = ["Dev Team <dev@example.com>"]license = "Apache-2.0"readme = "README.md"[tool.poetry.dependencies]python = "^3.10"flask = "^2.1"requests = ">=2.25,<3.0"[tool.poetry.group.dev.dependencies]pytest = "^7.0"black = {version = "^23.0", optional = true} # Example optional within group[build-system]requires = ["poetry-core>=1.0.0"]build-backend = "poetry.core.masonry.api"After manually converting it for
uv
, it would look like this:pyproject.toml # Converted uv-compatible pyproject.toml (After Migration)[project]name = "my-awesome-app"version = "1.2.3"description = "Does awesome things."authors = [{ name = "Dev Team", email = "dev@example.com" },]license = { text = "Apache-2.0" } # Or reference file: { file = "LICENSE" }readme = "README.md"requires-python = ">=3.10" # Translate Python constraint# Main dependencies moved heredependencies = ["flask>=2.1,<3.0", # Poetry's ^2.1 becomes >=2.1,<3.0"requests>=2.25,<3.0", # This constraint translates directly]# Dev dependencies moved here (standard location)[project.optional-dependencies]dev = ["pytest>=7.0,<8.0", # Poetry's ^7.0 becomes >=7.0,<8.0"black>=23.0,<24.0",]# Alternatively, uv also supports:# [tool.uv.dev-dependencies]# pytest = ">=7.0,<8.0"# black = ">=23.0,<24.0"# Build system - kept Poetry's, or could switch[build-system]requires = ["poetry-core>=1.0.0"]build-backend = "poetry.core.masonry.api"# --- OR using e.g. Hatchling ---# requires = ["hatchling"]# build-backend = "hatchling.build"# Notice the [tool.poetry] section is completely gone.Pay close attention to translating version specifiers (like Poetry’s
^
or~
) into the standard specifiers (>=
,<
,==
, etc.) expected in[project.dependencies]
. You might need to consult the PEP 440 specification for details. Using the standard[project.optional-dependencies]
for development dependencies is generally recommended for better compatibility with other tools, althoughuv
also directly supports[tool.uv.dev-dependencies]
.Tip: You could run
uv init
in a temporary directory to see the structureuv
expects inpyproject.toml
and use that as a template. - Project Metadata: Copy fields like
-
Generate the
uv
Lockfile: This is crucial.uv
will read your newly formattedpyproject.toml
and generate auv.lock
file from scratch. It completely ignores the oldpoetry.lock
file.Terminal window uv lock -
Sync Your Environment: Install dependencies based on the new
uv.lock
.Terminal window uv sync -
Cleanup: After testing and confirming the migration:
- Delete the old
poetry.lock
file. - Commit the modified
pyproject.toml
and the newuv.lock
. - Update CI/CD and other scripts: Replace
poetry install
withuv sync
,poetry run <cmd>
withuv run <cmd>
,poetry build
withuv build
(if using a compatible backend), andpoetry publish
withuv publish
.
- Delete the old
Migrating takes a bit of focused effort, especially converting pyproject.toml
from Poetry. However, the payoff is consolidating your workflow around uv
’s speed and simplicity for dependency and environment management moving forward. Remember to test thoroughly after migration!
Conclusion: A Simpler, Faster Python Future
Wrestling the Python environment hydra has felt like a rite of passage for too long. While tools like pyenv
and Poetry
were valiant efforts, the rise of uv
feels like a paradigm shift. By unifying version management, environment handling, dependency resolution, locking, and global tool installation into a single, lightning-fast binary, uv
drastically simplifies the Python developer experience.
Adopting the standalone uv
workflow, especially combined with shell integration for automatic environment switching, has significantly boosted my productivity and reduced daily friction. Projects initialize faster, dependencies install in seconds, and managing Python versions or CLI tools becomes trivial.
Is there a learning curve? A little, especially when migrating existing projects or setting up the shell integration. But the payoff – a development environment that feels fast, cohesive, and almost invisible – is well worth the initial effort.
Give uv
a try. Tame the hydra. Spend less time fighting your tools and more time building amazing things with Python. The future of Python development is looking remarkably fast and refreshingly simple.
References and Further Reading
- uv: Homepage | GitHub Repository | Installation Guide
- Ruff (Companion Linter/Formatter): Homepage
- Python Packaging Standards:
pyproject.toml
: pip docs- PEP 621 (Project Metadata): python.org
- PEP 517 (Build Backends): python.org
- PEP 518 (Build System Requirements): python.org
- Traditional Tools (Mentioned for Context):
- Shells: zsh | Bash | Fish
- Other Tools Mentioned: Homebrew | chezmoi | Rust | Xcode Command Line Tools
Disclaimer: The Python tooling landscape evolves rapidly. This post reflects the state and my recommendation as of April 2025. Always consult the official documentation for the latest features and commands.