The world of Python versioning (and the downstream package versioning) is wild. This StackOverflow thread gives you a sense of some of the core issues at play. (As an indication of the importance of the issue, even BDFL Guido van Rossum himself has the current second most upvoted answer.)

For a really vanilla and close-to-core-python setup, a combination of venv and pip seem to be the way to go. venv is part of the standard library and as such is pretty close to a default option.

For something a bit more involved, that handles dependencies and package installation in a slightly more deft manner, the combination of pyenv, pyenv-virtualwrapper and poetry works really well. I’ll detail some of the setup gotchas and usage patterns below.

pyenv for versioning Python itself

pyenv lets you install multiple versions of Python on the same machine. The interface to switch between local versions and whatever you’ve decided will be your global option is pretty intuitive.

Visit the pyenv github page for more on installation. (If you’re on a Mac you can simply do a brew install pyenv.)

To see which versions of Python you have installed locally:

pyenv versions

To see versions of Python which are available for installation:

pyenv install —list

Note that, as I understand it, these versions are not dynamically updated. You get an updated list of new Python versions by updating pyenv, in other words.

To install a specific version of Python, and to make it available for use:

pyenv install 3.9.1

To set that version of Python as the global version (i.e. running python will use this version by default):

pyenv global 3.9.1

If you are in a project directory and wish to only use a particular version of Python in that directory (and its subdirectories):

pyenv local 3.8.2

This creates a .python-version file in that directory with the desired local version.

pyenv-virtualenv for managing virtual environments

pyenv-virtualenv is a plugin that connects the work of selecting which version of Python to use (through pyenv, which we’ve previously installed) to the work of creating and running virtual environments to keep code contained in quasi-sandbox environments. When you install packages in virtual environments they don’t conflict with other locations where you might have conflicting versions of those same packages installed.

Read installation instructions and the docs here. (If you installed pyenv with homebrew, be sure to do the same with pyenv-virtualenv).

To create a virtual environment for the Python version used with pyenv, run pyenv virtualenv, specifying the Python version you want and the name of the virtual environment directory:

pyenv virtualenv 3.8.2 my-virtual-env-3.8.2

This will create a virtual environment based on Python 3.8.2 under $(pyenv root)/versions in a folder called my-virtual-env-3.8.2.

To list what virtual environments have been created and are available to use:

pyenv virtualenvs

As a common workflow pattern, you’d create your directory and cd into it, and then you can set the virtual environment you just created as the one to use for that directory:

mkdir test-project && cd test-project
pyenv local my-virtual-env-3.8.2

This should change the prompt in your terminal window and you’ll thus know that you’re now working out of that virtual environment. Any time you return to that folder you’ll automatically switch to that environment.

The manual way of turning on and off virtual environments is:

pyenv activate env-name
pyenv deactivate env-name

To remove a virtual environment from your system:

pyenv uninstall my-virtual-env

(This is the functional equivalent of removing the directories in $(pyenv root)/versions and $(pyenv root)/versions/{version}/envs.)

poetry for handling package installation and dependencies

python-poetry is the latest standard tool for handling package installations and dependency management.

You can use poetry without the previous two tools, but really they work best all together. Follow the installation instructions documented on their page to get it going.

Then update poetry:

poetry self update

poetry is one of those tools that’s able to update itself.

For basic usage for a new project, you can follow the following workflow. There are two ways to start a new project using poetry: using new or init. For example:

poetry new some-project-name

This will kickstart your new project by creating a bunch of files and a directory structure suitable for most projects, like so:

some-project-name
├── pyproject.toml
├── README.rst
├── some-project-name
│   └── __init__.py
└── tests
    ├── __init__.py
    └── test_some-project-name.py

You might want to use a src folder (above the some-project-name in our example) which is fairly commonly used, in which case amend the command as follows:

poetry new --src some-project-name

poetry init doesn’t do all the extra work of creating a directory and file structure. It merely creates a pyproject.toml file interactively, using some smart defaults. For a minimal use of poetry, this is definitely the way to go.

The add command adds required packages to your pyproject.toml and installs them (along with all their dependencies). It does a lot under the hood to make sure that dependencies are correctly resolving before installing. For example:

poetry add zenml

To add packages only to be used in the development environment:

poetry add --dev zenml

To list all installed packages in your current environment / project:

poetry show

To uninstall a package and remove it (and its dependencies) from the project:

poetry remove zenml

To install all relevant packages and dependencies of a project that you’ve newly cloned into:

poetry install

Note that it is possibly worth creating some custom scripts to handle some of the overhead of using these tools, depending on your common development workflows.