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:

$ 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:

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:

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

$ 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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# Logical conditions:
# 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:
# Without arguments it will create virtualenv named `.venv` with `python3.8` version
# $ ve
# or for a specific python version
# $ ve python3.9
# or for a specific python version and environment name;
# $ 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.

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

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

You can provide positional arguments like:

$ 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.

1
2
3
4
5
6
7
8
9
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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
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)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
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
}

Info

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