Automate Python Virtual Environment with a Script
Tags: python, bash, zsh
Published: 2022-04-27 18:41:20

---
title: Automate Python Virtual Environment with a Script
published: true
date: 2022-04-01 00:00:00 UTC
tags: python, bash, zsh
canonical_url: https://tech.serhatteker.com/post/2022-04/automate-python-virtualenv/
---

## Issue


There are 2 things that annoy me every time when I create a python virtual environment.

> NOTE
>
> Below instructions are related to the python's default **venv**  module.
>
> If you are using any other version/virtual environment management tools,
> like **Conda**, **Poetry**, **Pyenv**, below doesn't apply.


The first one is that I have to type every time:

```bash
$ python -m venv ./.venv
$ . ./.venv/bin/activate
# or
# $ source ./.venv/bin/activate
```

The second thing is that after installing any *package* I'm getting below `pip`
version *WARNING*:

```bash
WARNING: You are using pip version 21.1.1; however, version 22.0.4 is available. You should consider upgrading via the '/path/to/project/.venv/bin/python3.8 -m pip install --upgrade pip' command.
```

It is an expected behavior, 'cause `python -m venv` calls `python -m ensurepip` to install `pip`.

And there is no way to globally upgrade since `pip` is installed with *bundled* version which is almost out of date. You can check it like below:

```python
>>> import ensurepip
>>> ensurepip.version()
'21.1.1'
```

Even though you can't upgrade to the latest version with below commands as mentioned in [ensurepip] doc:

```bash
$ python -m ensurepip --upgrade
```

Because `ensurepip` will only install the *bundled* version even with the `--upgrade` option.

There is no official option to update the *bundled* `pip` and `setuptools`, yet.


## Solution

Okay, since we are developers, we always find our ways and produce our solutions that suits our needs.

My solution comes from a shell script. You can put it in your shell configuration file, like `.zshrc`, `.bashrc`, `config.fish`.

With below function, when I enter `ve` command, it will create my virtual environment — if it doesn't exist, activate it and upgrade to the latest `pip` version.

```bash
# 0. If not already in virtualenv:
# 0.1. If virtualenv already exists activate it,
# 0.2. If not create it with global packages, update pip then activate it
# 1. If already in virtualenv: just give info
#
# Usage:
# $ ve
# or
# $ ve python3.9
# or
# $ ve python3.9 ./.venv-diff
ve() {
    local py=${1:-python3.8}
    local venv="${2:-./.venv}"

    local bin="${venv}/bin/activate"

    # If not already in virtualenv
    # $VIRTUAL_ENV is being set from $venv/bin/activate script
	  if [ -z "${VIRTUAL_ENV}" ]; then
        if [ ! -d ${venv} ]; then
            echo "Creating and activating virtual environment ${venv}"
            ${py} -m venv ${venv} --system-site-package
            echo "export PYTHON=${py}" >> ${bin}    # overwrite ${python} on .zshenv
            source ${bin}
            echo "Upgrading pip"
            ${py} -m pip install --upgrade pip
        else
            echo "Virtual environment  ${venv} already exists, activating..."
            source ${bin}
        fi
    else
        echo "Already in a virtual environment!"
    fi
}
```


This function may seem complex, so let me walk through it line by line.

`$1` is the first argument, the desired python version, `$2` is the second argument which is virtual environment's name/directory, provided to the function `ve()`. If no arguments provided function will use default values: `python3.8` and `.venv`. You can these default values according to your needs.

```bash
ve() {
    local py=${1:-python3.8}
    local venv="${2:-./.venv}"

    local bin="${venv}/bin/activate"
    ...
}
```

You can provide positional arguments like:

```bash
$ ve python3.9 .venv2
```

After creating local `variables`, if we are already in a *virtual environment*, it just gives "already in a virtual environment" message. It decides this whether we're in a *virtual environment* by checking if there is a **environment variable** called `$VIRTUAL_ENV` exported from `activate` — `.venv/bin/activate`, script.

```bash
ve() {
    ...

	if [ -z "${VIRTUAL_ENV}" ]; then
        ...
    else
        echo "Already in a virtual environment!"
    fi
}
```

If we're not in a *virtual environment*, it will control if `.venv` directory exists. If the directory exists, it will be activated:

```bash
ve() {
    ...

	if [ -z "${VIRTUAL_ENV}" ]; then
        if [ ! -d ${venv} ]; then
            ...
        else
            echo "Virtual environment  ${venv} already exists, activating..."
            source ${bin}
        fi

    else
        ...
    fi
}
```

However, if `.venv` directory does not exist, it will:
- Create virtual environment `.venv` with global packages included (line 7),
- Add exporting desired python version environment variable into `activate` script (line 8)
- Activate the virtual environment (line 9)
- Upgrade the current `pip` version to the latest one (line 11)


```bash
ve() {
    ...

	if [ -z "${VIRTUAL_ENV}" ]; then
        if [ ! -d ${venv} ]; then
            echo "Creating and activating virtual environment ${venv}"
            ${py} -m venv ${venv} --system-site-package
            echo "export PYTHON=${py}" >> ${bin}    # overwrite ${python} on .zshenv
            source ${bin}
            echo "Upgrading pip"
            ${py} -m pip install --upgrade pip
        else
            ...
        fi

    else
        ...
    fi
}
```

> You probably don't need *line 8*, I'm adding this, because using this environment variable in my other `aliases`/`functions`/`binaries`.


## Conclusion

In the end, with this shell script we solved our annoying *bugs* and make us more productive again.

You can find this script in this [gist] or in [function] from my [dotfiles] repo.

All done!


---
**Changelog**

- 2022-05-10 : Added gist and dotfiles link references




[ensurepip]: https://docs.python.org/3/library/ensurepip.html#module-ensurepip
[gist raw]: https://gist.githubusercontent.com/SerhatTeker/899461f3bcf7b82e115f3ff01a49b21d/raw/ve
[gist]: https://gist.github.com/SerhatTeker/899461f3bcf7b82e115f3ff01a49b21d
[function]: https://github.com/SerhatTeker/dotfiles/blob/cef51a994dda18935006d6bc3055bf7eb58fe43d/zsh/.functions#L327-L362
[dotfiles]: https://github.com/serhatteker/dotfiles
surepip