Skip to main content

Command Palette

Search for a command to run...

How I manage my Python Virtual Environments

Published
6 min read
How I manage my Python Virtual Environments
Y

Backend Engineer 🚀 Cloud Native Enthusiast ☁

In a common workflow we create a virtual environment using different tools inside of the project directory. You might have venv, .venv, env and so on. I find it a bit cluttered. My approach is a bit different, and you might already be using something similar with a third party tool.

Here's how it goes, all the virtual environments go inside of the central ~/.venvs directory. If I am inside of a project called project-xyz, I create a virtual environment inside of the central directory like ~/.venvs/project-xyz. I have one-to-one mapping of my project directory name with my virtual environment name and it comes with some benefits and shortcuts.

Creating Virtual Environments

I have a zsh function that helps me build a virtual environment using the python version of my choice. And yes I use uv.

This is what I have in my ~/.zshrc

vm() {
    base_name=\((basename "\)PWD")
    uv venv --python 3.\({1:-11} ~/.venvs/"\)base_name"
}
  • vm: name of the function, will also automatically become a SHELL alias

    For me everything relating to virtual environment starts with v so it's "virtual environment make", aka vm

  • base_name=\((basename "\)PWD"): we are taking the base name of the "present working directory (pwd)" which will be the name of the project directory.

    example:

    if pwd is "/home/yankee/projects/tldr-generator"

    then basename is tldr-generator

  • uv venv --python 3.${1:-11}: create a virtual environment using uv. Set the python version that defaults to 11 and if I want to use any other version it will take the first argument passed to the alias.

    example:

    if I run vm 13, it will create a virtual environment with python version 3.13. At times I feel lazy to write the whole 3.13 so I just pass the minor version I want to use.

  • ~/.venvs/"$base_name": The later part is just the location where I want to create my virtual environment.

Now with this command, I can to go to any project directory, then run:

vm

# OR
# for python 3.13
vm 13

# for python 3.12
vm 12

# and so on...

Activating Virtual Environments

Adding on to the idea of having the base_name as the virtual environment on the centralized location. Any time I am in the project directory I can easily activate the virtual environment from ~/.venvs/<base_name>/bin/activate. For that I have a function called va (virtual environment activate).

va() {
    base_name=\((basename "\)PWD")
    source ~/.venvs/"$base_name"/bin/activate
}

Now if I am in any of my project directory, I can simply run va and it will activate the correct virtual environment for the project. And if it does not exist, I can do vm followed by va.

Deactivating

To deactivate a virtual environment, one can simply write deactivate but it ‘s a long word to type and mistakes have happened. So following the pattern, I have an alias for this one called vd (more about aliases in this video)

alias vd="deactivate"

Deletion

One common action I have to do sometimes is delete a virtual environment and build a new one again. For deletion I have an alias called vrm that deletes the existing virtual environment.

vrm(){
    base_name=\((basename "\)PWD")
    venv_path="\(HOME/.venvs/\)base_name"
    rm -rf "$venv_path"
    echo "Deleted $venv_path"
}

Same idea with basename to find the virtual environment and delete it. Now with creation and activating the new venv, the whole flow looks like this:

vrm 
vm 
va
uv sync # or whatever package installer action

Putting it all together

Why do this?

  • Birds-eye view of all virtual environments and deletion: most of the time there are projects with venv that takes huge amounts of storage. They are just there sitting idle and it’s hard to find them. With everything under ~/.venvs I have an birds-eye view of which project is consuming what amount of storage. And if it’s taking too much storage or I no longer need it, I can delete it from there. We can do this with a simple built-in du command or you can point your storage manager app here.

    du -sh */ | sort -hr
    
    • -sh: shows size of each directory in human-readable form

    • sort -h → sorts by size (handles KB, MB, GB correctly)

  • Good with git worktrees, kinda*: When I am working with multiple features (mostly two at a time) for a project, I usually work on the main worktree and create a new worktree to hack on a different feature. So the way I create worktrees is similar to virtual environments, where things are kept central. The pattern I follow is:

    ~/.worktrees/<feature-name>/<project-name>
    

    Here the project-name is same as the main project directory name (basename). For va to work, we explicitly need to match the name of the project directory, hence I create a nested directory structure where feature branch is a parent directory that contains the main project directory.

    This way I can switch to my worktree, run va again, and by nature of finding virtual environment and activating based on the basename, it will automatically activate the existing virtual environment that I created from my main worktree.

    Activating the same virtual environment with different worktrees

    I have a dedicated tool created to switch between worktrees with breeze called git-worktree-switcher. And to see how you can work with git worktrees and make use of this tool to be more effective you can check my dedicated video:

https://www.youtube.com/watch?v=fRPpM5kvlls

So the `kinda*` is, sometimes the feature that I am building requires a version upgrade, addition or removal of a package. In such cases using the same virtual environment across two different feature branch creates chaos. When this happens I don’t bother creating a project directory nested inside the feature directory. I simply create a worktree that does not match the main worktree project directory name and create a new virtual environment for it.

```shell
git worktree add -b new-feature-x ~/.worktrees/new-feature-x
```
  • Faaaast and error free: with all this zsh/bash function and aliases I can go about my day to day dealings with virtual environment and worktrees super fast. I have been using this workflow for couple of years now and these aliases has helped me immensely to type less and make less mistakes. You are very less likely to mistake a two letter alias compared to writing a whole command or a tab autocompletion suggestion which might have had a typo.