python: command not found on Ubuntu 20.04, and Why You Should Not Symlink It
Ubuntu 20.04 came out last week and the first machine I built on it broke a deploy script that has worked since 2015. Not the application, the deploy script, on the very first line of a helper it calls:
./deploy.sh: line 42: python: command not found
Or, when a script has the interpreter in its shebang:
/usr/bin/env: 'python': No such file or directory
There is no /usr/bin/python on a clean 20.04 install. Not a broken one, not one pointing somewhere odd. It is absent. python3 is there, at version 3.8, and that is the whole story.
My view is that Ubuntu made the right call, and that the popular fix for it is wrong. The name python has meant Python 2 for twenty years. Python 2 reached end of life in January. Silently repointing an ambiguous command at a different language would have been worse than removing it, because a script written for Python 2 would then run under Python 3 and fail somewhere in the middle, after doing half its work. An absent command fails on line one, before anything happens. That is a better failure.
Pick by situation, not by habit:
- Your own scripts: change the shebang to
#!/usr/bin/env python3and callpython3explicitly. This is the real fix. - Your own application: run it in a virtualenv, where
pythonalways exists and points at the right interpreter regardless of the host. - A workstation where everything is Python 3:
sudo apt install python-is-python3. - A legacy application you cannot port yet:
sudo apt install python2, and callpython2by name.
Do not create the symlink by hand with ln -s.
Why the command is missing rather than repointed
For most of Python's history, the packaging convention was explicit that python meant Python 2, and it was updated only recently to let distributions point it at Python 3 or leave it out entirely. Ubuntu chose to leave it out for the release that ships after Python 2 stopped being maintained.
The result is a clean split. python3 is the interpreter, python3-pip is the package installer, and everything in the archive that used to be python-something is now python3-something. There is no ambiguous name left in the middle for a script to guess at.
What you get on a fresh 20.04 server:
$ which python3 python
/usr/bin/python3
$ python3 --version
Python 3.8.2
$ ls -l /usr/bin/python*
lrwxrwxrwx 1 root root 9 Mar 13 12:45 /usr/bin/python3 -> python3.8
-rwxr-xr-x 1 root root 5490488 Mar 13 12:45 /usr/bin/python3.8
Step 1: find out what is calling python
Before installing anything, find out which code is asking, because that decides the fix:
grep -rn 'python' --include='*.sh' /srv /opt /usr/local/bin 2>/dev/null | grep -v python3
grep -rln '^#!.*python$' /usr/local/bin /srv
crontab -l | grep python
In my case it was four cron entries and a wrapper script, all of them mine, all of them fixed by adding a single character.
Step 2: fix the shebangs you own
#!/usr/bin/env python3
Prefer env python3 over a hardcoded /usr/bin/python3 so the script also works inside a virtualenv, where the right interpreter is earlier on the path. This is the fix that keeps working when you move to the next release, and the next.
Step 3: use a virtualenv for anything deployed
sudo apt install -y python3-venv
python3 -m venv /srv/app/venv
/srv/app/venv/bin/python --version
Python 3.8.2
Note that python exists inside the virtualenv, as venv/bin/python, pointing at the interpreter the venv was created with. Once your service, your cron jobs and your deploy script all use absolute paths into the virtualenv, the question of what the system python means stops applying to your application entirely. That is the position you want to be in, on every distribution, forever.
Step 4: only if you need the bare command
sudo apt install -y python-is-python3
python --version
All this package does is install /usr/bin/python as a symlink to python3, but it does so through dpkg, which means the package manager knows the file exists, upgrades handle it, removing it puts things back, and installing python2 later does not produce a mess.
For a legacy application that genuinely needs the old interpreter:
sudo apt install -y python2
python2 --version
There is no packaged pip for it, so if you need one you bootstrap it with the get-pip.py script for 2.7. Treat this as a bridge with an end date, not part of a normal server build. I set out how I plan those ports in last year's piece on the Python 2 sunset, and everything in it applies with more urgency now that the sunset has passed.
The same break, in build images and CI
The failure moves around once you leave the server. A Dockerfile that starts FROM ubuntu:20.04 and runs apt-get install -y python now fails at build time:
E: Package 'python' has no installation candidate
There is no package called python any more. Install python3 and, if the image's own scripts call the bare name, python-is-python3 alongside it:
FROM ubuntu:20.04
RUN apt-get update && apt-get install -y --no-install-recommends \
python3 python3-venv python3-pip \
&& rm -rf /var/lib/apt/lists/*
The same applies to CI runners and to any provisioning tool that assumes an interpreter is present. Ansible in particular connects to a host and needs a Python on the far end; if your inventory has been relying on the discovery of /usr/bin/python, set the interpreter explicitly to /usr/bin/python3 for the 20.04 hosts rather than installing a symlink on every machine to satisfy the control node.
Verification
python3 --version
command -v python || echo "no bare python, which is fine"
sudo systemctl restart yourapp && systemctl is-active yourapp
sudo run-parts --test /etc/cron.daily
Then trigger, by hand, every cron job that mentions Python. A deploy script that fails is noticed in ten minutes. A nightly job that fails is noticed when someone asks for a report that has not been generated since the upgrade.
What people get wrong
ln -s /usr/bin/python3 /usr/bin/python. This is the top answer everywhere and it creates a file that dpkg does not own. Later, python-is-python3 or python2 will not install cleanly over it, an upgrade may leave you with a dangling link, and nothing records why the file is there. Same result, no bookkeeping. Use the package.
update-alternatives for python. Alternatives is designed for genuinely interchangeable implementations, like editors or Java runtimes. Python 2 and Python 3 are not interchangeable, which is the entire reason the command was removed. You also now have two mechanisms competing to own the same path.
Installing python2 to make an error go away. If the software is yours, you have just postponed the port and added an unmaintained interpreter to a production server. Do it only for third party software you cannot change, and write down the date you will revisit it.
Assuming pip means pip3. On 20.04, install python3-pip and use pip3, or better, python3 -m pip inside a virtualenv, which removes any doubt about which interpreter is receiving the package.
When it is still broken
- bad interpreter: No such file or directory. The shebang hardcodes a path that does not exist on this machine. Change it to
env python3, and check for Windows line endings while you are there, since a trailing carriage return produces the same message with an invisible cause. - A module imports on the command line but not in the service. Two interpreters. The service is using the system Python and you tested in a virtualenv, or the other way round. Print
sys.executablefrom inside the failing process and stop guessing. - You need a version other than 3.8. The deadsnakes PPA carries additional interpreter versions for Ubuntu, installed alongside rather than replacing the system one.
- Something in the system stopped working after your symlink. Remove the hand made link, install the package instead, and check
/usr/bin/pythonis where dpkg expects it withdpkg -S /usr/bin/python.
Frequently asked questions
- Why is there no python command on Ubuntu 20.04?
- Because the name python has meant Python 2 for twenty years, and Python 2 reached end of life in January 2020. Rather than silently repoint an ambiguous command at Python 3, Ubuntu 20.04 ships python3 only and leaves /usr/bin/python absent unless you install a package that provides it.
- What does the python-is-python3 package do?
- It installs a symlink at /usr/bin/python pointing to python3, under dpkg's control so upgrades and removals stay consistent. It is the supported way to get the bare python command back on a machine where everything is genuinely Python 3.
- Can I still install Python 2 on Ubuntu 20.04?
- Yes, the python2 package is in the archive and gives you /usr/bin/python2. It receives no upstream security fixes, so treat it as a bridge for one legacy application while you port it, not as a normal part of a new server build.
- Is ln -s /usr/bin/python3 /usr/bin/python safe?
- It works today and creates a file that the package manager does not know about. Later, installing python-is-python3 or python2 conflicts with it, and any script that still expects Python 2 semantics now runs under Python 3 and fails in confusing ways. Install the package instead.