DEV Community

Aaron Steers
Aaron Steers

Posted on

The Modern Ways to (not) Install Python and Virtual Environments

TL;DR: You can use uv for everything now. Just brew install uv and then you can forget about brew, pyenv, pipx, pip, and all the others.

Where we are today

The Python ecosystem has changed a lot over the past few years. What used to be best practices are quickly becoming anti-patterns. The biggest challenge I see with folks using Python today is that they are stuck in old patterns that cost them both time and pain.

The Outlawed Patterns List

If you are using any of these patterns, please stop immediately.

  • Everything related to pyenv.
  • Calling pip directly, for any reason.
  • Calling python directly, and/or expecting python on PATH to be a specific version.
  • Anything related to venv, virtualenv, or activate, or manually creating or activating virtual environments in any way.

The Modern Way to Install Python

Note: You probably don't actually need to install Python before you start. In less than 2 seconds, the uv tool (as you'll see below) can install Python versions on-demand for apps, projects, and scripts. So you can probably skip this step entirely. But if not:

# Install uv or update it if it's already installed:
brew install uv

# Show the versions of Python available and/or installed:
uv python list

# Install a couple Python versions
uv python install 3.13
uv python install 3.12

# Set your global python version (`python` on `PATH`)
uv python install 3.13 --global --preview
Enter fullscreen mode Exit fullscreen mode

The Modern Way to Manage Virtual Environments

You should never need to manually create a virtual environment again. The manner in which you create your virtual environment will depend on whether you are running an app, writing code for a project, or writing a script. But for all of the below patterns, the virtual environment is created automatically, with no way to mess up, and no way to forget to activate or deactivate an environment.

Identify your use case from one of these three patterns, and apply the suggested pattern:

1. Installing a Python App

Simply uv tool install my-tool and then you'll have my-tool on your PATH.

2. Running a Python App

Skip the install step and use uvx: uvx my-tool --version

This works from anywhere you have uv installed.

You can optionally set the python version to something explicit and uv will lightning-fast bootstrap that version of Python if it isn't already installed:

uvx --python=3.12 my-tool --version

You can also pin to a specific version of your tool or force the "latest" on each run:

uvx my-tool@latest --version
uvx my-tool@3.2.1 --version
Enter fullscreen mode Exit fullscreen mode

3. Write Python Code for a Project

Simply use uv init to create a new project directory, uv add to add dependencies, and uv run to run them. Unlike it's predecessor, Poetry, you don't even have to explicitly install anything, since uv run implies uv sync.

# Create a `repos` directory if you don't have one yet:
mkdir ~/repos
cd ~/repos

# Create the `uv` project scaffold:
uv init my-new-project
cd my-new-project

# Optionally set the Python version you want for this project:
uv python pin 3.13

# You can immediately run the sample without any extra steps
uv run main.py

# You can add dependencies (replaces `pip install`):
uv add my-other-dependency-a my-other-depedency-b

# Dependencies are auto-installed whenever you call `uv run`:
uv run main.py
Enter fullscreen mode Exit fullscreen mode

Write a Portable Python Script

Sometimes you don't want a full project, or you need to have many scripts each with their own dependencies. This is easy to do with uv.

Take this example my_helper_script.py:

#!/usr/bin/env -S uv run --script
#
# Inline dependency declaration (PEP 723):
# /// script
# requires-python = ">=3.10"
# dependencies = [
#   "my-dependency-a",
#   "my-dependency-b",
# ]
# ///
#
# Usage with uv CLI:
#    uv run --script ./my_helper_script.py
#
# With the custom shebang, you can also invoke directly:
#    ./my_helper_script.py
#
import my_dep_a
import my_dep_b

def main()
    print("Hi, there!")

if __name__ == "__main__":
    main()
Enter fullscreen mode Exit fullscreen mode

Now you have a script that can run anywhere, on any version of Python (no Python install necessary), with your dependencies automatically bootstrapped and fully isolated from other Python environments and potential version conflicts. 🎉

What did I miss?

I'll update the article with any tips or tricks that others share in comments. Comment down below if I missed anything.

Top comments (0)