This is how to set up the One True Development Environment for Python.
Haha, just kidding, there is no such thing. Here's one way to do it that works for me, and an attempt to explain the benefits of doing it this way.
Different developers working on the same project can choose different ways of doing these things, so we don't want to set up the project so it can only be worked on this way. The approach I use has worked well in a variety of settings.
This post is inspired by Jacob Kaplan-Moss's post My Python Development Environment, 2020 Edition.
Common Requirements
- Projects use different Python versions.
- Python 2 is dead. This assumes a recent Python 3 version. (Say, 3.6 and later?)
- As far as possible, works the same on Mac and on different Linux distributions (though I only do Python development on Ubuntu).
- No dependency on any particular IDE or editor.
My experience is primarily on Ubuntu. Please let me know if anything here doesn't work on other Linuxes, or on Mac.
Installing Python
Assumptions:
- We need different versions of Python for different projects.
- We don't want to be bothered with the changes our operating system might have made to how Python is installed, or to risk breaking our operating system by messing with the system Python.
Therefore, we will not use the operating system installed copy of Python, even if it happens to be the version we want.
Compile It Myself?
For a while, I was using an Ansible role to download source, compile, and install each version of Python that I needed (on Linux), and using stow to put them all under /usr/local. Then if I wanted Python 3.8, I could just run python3.8 and it would run the right one.
Except that if I had one project that required Python 3.8.1, and another that needed Python 3.8.2, I was out of luck. When I build and install Python vX.Y.Z, the installer creates executables pythonX and pythonX.Y, but not pythonX.Y.Z. If I had multiple Python 3.8's installed, I didn't know which 3.8.x version I'd get by running python3.8. (Actually, stow would stop me from installing multiple Python 3.8's in the first place, since there would be duplicate files.)
To create the right virtualenv, I would have to type something like:
$ /usr/local/stow/python3.8.6/bin/python -m venv ...
Pythonz?
So, I switched to using pythonz. It automates the install — and uninstall — of any version x.y.z of python I want (just run pythonz install 3.7.7).
Pythonz doesn't put all those Python versions on the path. Instead, I can find a particular executable by running:
$ pythonz locate 3.7.7 /home/dpoirier/.pythonz/pythons/CPython-3.7.7/bin/python3
So I can run a specific version using something like this:
$ $(pythonz locate 3.7.7)
where pythonz locate 3.7.7 prints the complete path to the Python 3.7.7 executable, and the $( ) captures the output and executes it.
I could use that when creating a virtualenv for a project, and not have to deal with pythonz afterward. Still, it always seemed inelegant.
Pyenv
Now I'm trying pyenv. Like pythonz, I can easily install multiple Python versions (pyenv install 3.7.7). But it takes a completely different approach to selecting which version to run. You put pyenv's shims directory first on your path, so that when you run any python command, you are running pyenv's shim executable for that command. That shim executable figures out the right actual python executable to run, and invokes it for you.
You can tell the shim which version you want at any given time in several ways.
- You can set PYENV_VERSION in your shell.
- You can put the version in a .python-version file in your current directory.
- You can have a .python-version file in any parent directory and it'll use the first one it finds, working its way up.
- Finally, you can configure a default version to use as a fallback.
This looks like it'll fit into my existing workflow pretty well. I already use direnv, so I can just set PYENV_VERSION in my .envrc file and get the version I want without changing anything in the project's files in source control. Or, I could create a .python-version file in the project, which shouldn't affect any user not using pyenv, but whose meaning should be pretty obvious.
Creating a virtualenv
Python has had built-in support for creating virtual environments since version 3.3, so we'll use that to create our virtualenv.
Where should we put our virtualenv, though? I used virtualenvwrapper for a long time, which puts all of your virtual environments in one directory ($HOME/.virtualenvs by default).
But virtual environments tended to accumulate in my virtualenvs directory from projects I hadn't touched in years, and it bugged me.
More recently, if I'm working on a project in .../projectname (my top-level directory where I cloned the project from git), then I create the virtualenv at .../projectname.venv. Anytime I'm cleaning up an old project, I'll see the virtualenv next to it and remember to clean that up too.
(I don't like to put the virtualenv actually inside my Python project, because then my IDE feels compelled to index everything in it, do searches through all of it, etc. Otherwise, I might just put my virtualenv at .../projectname/venv.)
Putting my virtualenv where virtualenvwrapper doesn't expect it does mean I can't use virtualenvwrapper's workon command to switch virtualenvs, but that's okay. I had already stopped using the rest of virtualenvwrapper when python -m venv started working.
I use direnv already, so I just have a little script that creates a virtualenv at ../projectname.venv and also adds a line like . ../project.venv/bin/activate to my .envrc file. Then anytime I change to that directory, my virtual environment is already activated.
(I'm aware of pyenv-virtualenv and pyenv-virtualenvwrapper, but these look like they also hide away the virtual environment directories somewhere I'll forget about them, so for now, I'm not using them.)
Installing Packages Into the virtualenv
I've played with pip-tools for installing Python packages into virtual environments, but somehow, most of my projects still just use pip to install requirements:
$ pip install -r requirements.txt
What About "tox"?
tox needs to be able to find each version of Python mentioned in tox.ini, and it doesn't know to ask pyenv for them. But you can expose as many python versions as you want using pyenv local. So I can, for example, set .python-version to:
3.7.5 3.8.6
and have tox work for test environments py37 and py38.
Where Things Stand
I think this is one of the best environments I've used over the years, but as you've probably gathered, I'm always looking for ways to improve it. I continue to watch pipenv and pip-tools, I keep an eye on Python releases for improvements to what Python provides out of the box, and I look for ways to better integrate with my favorite IDE, Pycharm.