Automatic Python Environments with Mise + uv
Have you ever installed a Python package thinking you were in a virtual environment, only to realize you accidentally messed up your system installation? After doing this one time too many, I decided to improve my workflow! This post is going to cover my personal setup: a mix of mise and uv, alongside some minor shell magic!
Virtual Environments
A Python virtual environment acts as an isolated set of packages, typically for a specific Python project or number of projects. When it comes to creating an isolated environment there are many popular options, each with positives and negatives:
- Virtual Environments
- Ideal for simple Python projects, very minimal overhead.
- Small and lightweight, support is included in Python by default.
- Provides easy integration with
piptooling and other standard tools.
- Conda Environments
- Much higher focus on reproducible builds, as a priority.
- Includes support for more than just Python packages, e.g. system packages.
- Often conflicts with existing
pipinstallations and requires new tools.
- Docker Environments
- Provides complete environment control (OS, services, etc.).
- Requires a completely separate workflow to build images.
- Very popular for production deployments.
While there are many other tools and alternatives, these are three of the most popular for comparison. The best choice will depend on your use case, preferred development workflow, or even company requirements. For my personal development workflow, I have a few requirements:
- An efficient feedback loop: no waiting for builds or lengthy installation processes.
- Interoperability with
pip, to easily integrate with coworkers and standard tooling. - Although not a requirement, reproducibility would be a nice bonus if we can get it.
Due to Docker being much more involved, and Conda having conflicts with pip, I stuck with basic virtual environments and explored further. One of the most popular tools for dealing with virtual environments is uv, which is gaining support due to significantly faster performance and stability.
Creating an Environment
The uv tool uses a virtual environment implementation compatible with that of standard Python, but provides additional layers of tools for better speed and reproducibility. Although I highly recommend looking into the bells and whistles of uv, for this blog post we'll stick to package isolation:
$ uv venv
Using CPython 3.11.14 interpreter at: /tmp/workspace
Creating virtual environment at: .venv
Activate with: source .venv/bin/activate
Creating a new virtual environment is quick and easy, and activating it is as simple as running the command printed above. Creating a new shell session or deactivating your environment will return to using packages from the global Python installation.
The recommended uv workflow is focused on reproducibility and differs heavily from what you might be used to with pip. As a convenience, the authors choose to ship uv with a pip compatible implementation, giving you the familiar interface:
$ uv pip install pydantic
Resolved 5 packages in 1.03s
Installed 5 packages in 11ms
+ annotated-types==0.7.0
+ pydantic==2.12.5
+ pydantic-core==2.41.5
+ typing-extensions==4.15.0
+ typing-inspection==0.4.2
As you can see from the above, an immediate bonus of uv pip is that it'll run much faster than standard pip. With that being said, there are still cases where you might need a "real" copy of pip around. This can be done using the UV_VENV_SEED option before creating your environment:
$ export UV_VENV_SEED=1
If you are just getting started with uv or don't have any interest in using the other features of uv, I highly recommend setting this value inside your shell profile and forgetting about it. Using the default pip instead of uv pip without this setting will likely end up calling your global pip and accidentally installing packages globally!
Mise Integration
Now that we have a solution for package isolation, the next step is to figure out how to isolate Python itself. Although uv itself provides ways to manage your Python version, I'd prefer to use mise instead as I already use this for other languages.
Mise is a tool that allows you to easily control and activate versions of tools (e.g. programming languages) inside specific directories automatically. Luckily for us, uv is popular enough that mise supports it alongside Python out of the box. This makes it very simple to have mise manage both tools inside the mise configuration:
[tools]
python = "3.11"
uv = "latest"
[settings]
python.uv_venv_auto = true
Once configured, these settings will have mise link dependencies and shims inside a project automatically if it finds a uv.lock file inside your working directory. This is extremely convenient as it removes the need to explicitly activate your virtual environment. This completely eliminates the pain of forgetting to do so, which would otherwise cause you to accidentally install things globally.
Shell Configuration
By this point we have a pretty neat setup! We have a virtual environment created via uv that is activated automatically via mise. This is pretty cool and works well, but there are still some areas to improve:
- Some projects don't use
pyproject.toml(required foruv). - Scripts and tools are not necessarily part of a "project" at all.
- Forgetting to run
uv venvwill quietly be ignored.
There are many ways to solve these problems, but I personally chose to do this by hooking into my shell prompt. By doing this I can guarantee that any command I run inside a Python project is isolated automatically, and without having to think about it.
PROMPT_COMMAND="_setup_python_env"
_setup_python_env () {
# initalize virtual env for python projects
if [[ -f pyproject.toml || -f requirements.txt ]]; then
if [[ ! -d .venv ]]; then
uv venv
fi
fi
# initialize lockfile for mise if missing
if [[ -d .venv && ! -f uv.lock ]]; then
printf 'version = 1\nrequires-python = ">=3.10"\n' > uv.lock
fi
# Evaluate the mise hook if the env is not detected in the path
if [[ -f uv.lock && ":$PATH:" != *":$PWD/.venv/bin:"* ]]; then
eval "$(mise hook-env -f)"
fi
}
As I'm mainly a bash user, the code above makes use of PROMPT_COMMAND, but there are similar hooks in other shells (e.g. precmd in zsh). This custom PROMPT_COMMAND will have my shell run _setup_python_env upon each prompt, which runs through a couple of simple steps:
- Check for either a
pyproject.tomlor arequirements.txt. - Lazily create a virtual environment if either of these files exists.
- Lazily create a
uv.lockif the directory contains a virtual environment. - Reinitialize the local mise environment if
This means that you'll always have a virtual environment in your Python projects, regardless of whether you're using a uv or a pip based project. With this in place, combined with the settings from earlier, you can choose whether to lean into using uv or simply continue to use pip directly.
So far I'm really liking this setup, as I get the speed of uv when I want it but everything remains compatible with scripts using pip directly. Regardless of whether you're working on a script or a larger project, dependencies stay cleanly isolated without having to think about it!