# How I manage my Python Virtual Environments

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](https://docs.astral.sh/uv/getting-started/installation/).

This is what I have in my `~/.zshrc`

```shell
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:

```shell
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).

```shell
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](https://www.youtube.com/watch?v=Ww3IfxEut4M))

```shell
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.

```bash
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:

```bash
vrm 
vm 
va
uv sync # or whatever package installer action
```

## Putting it all together

![](https://cdn.hashnode.com/uploads/covers/609a020321068818a5c65045/b4b98707-5c58-4ba2-a2ec-65399f235a3b.gif align="center")

## 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.
    
    ```shell
    du -sh */ | sort -hr
    ```
    
    *   `-sh`: shows size of each directory in human-readable form
        
    *   `sort -h` → sorts by size (handles KB, MB, GB correctly)
        
        ![](https://cdn.hashnode.com/uploads/covers/609a020321068818a5c65045/5999ca05-6630-4573-9b2e-759ac47256af.png align="center")
        
*   **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:
    
    ```shell
    ~/.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](https://cdn.hashnode.com/uploads/covers/609a020321068818a5c65045/f575111c-68cf-4d9e-80bc-f74309768424.gif align="center")
    
    I have a dedicated tool created to switch between worktrees with breeze called [git-worktree-switcher](https://github.com/yankeexe/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.
