</>CodeWithKarani

python: command not found on Ubuntu 20.04, and Why You Should Not Symlink It

Karani GeoffreyKarani Geoffrey6 min read

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 python3 and call python3 explicitly. This is the real fix.
  • Your own application: run it in a virtualenv, where python always 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 call python2 by 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
Who owns the thing that typed "python"? Code you control scripts, cron jobs, deploy tooling Say python3 explicitly #!/usr/bin/env python3 Your application web app, worker, anything deployed Use a virtualenv venv/bin/python exists, host does not matter Your own workstation nothing left that needs Python 2 apt install python-is-python3 a symlink dpkg knows about Third party legacy software apt install python2, and plan the port
Four different answers. The mistake is applying the workstation answer to a production server.

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.executable from 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/python is where dpkg expects it with dpkg -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.
#Ubuntu#Python#Linux#Upgrade#Deployment
Keep reading

Related articles