Python 2 Sunsets on January 1: What It Means and How to Get Off It
Python 2 has nine days left. The date isn’t a surprise, but plenty of code nobody thinks about still runs on it: a cron job someone wrote in 2012, a deploy script, a build step that shells out to python and quietly gets 2.7. If any of that is yours, it’s worth being precise about what “sunset” means and what to do about it.
I’m sticking to what the Python core developers, the Python Software Foundation, and the distribution vendors have published themselves.
What the core team said sunset means
The plainest statement is python.org’s Sunsetting Python 2 page. It says the volunteers who maintain Python decided January 1, 2020 is the day they sunset Python 2, and that after that day they won’t improve it, even if someone finds a security problem in it. The advice is to upgrade to Python 3 as soon as you can.
The same page explains how we got here. The sunset was first announced in 2008 for 2015, and in 2014 it was pushed back to 2020 because so many people hadn’t moved. It also spells out the consequences: if catastrophic security problems turn up in Python 2 or in software written for it, most volunteers won’t help fix them, and that help will only get scarcer over time.
PEP 373, the Python 2.7 release schedule, says the same thing in release manager terms: support officially stops January 1, 2020, but the final release will happen after that date. As of this week, the PEP lists a 2.7.18 code freeze in January 2020, a release candidate in early April, and the final 2.7.18 in mid April. Those are plans, not releases. PEP 404 has also been around since 2011 to make the other half clear: there will never be an official Python 2.8, and the upgrade path from 2.7 is Python 3.
The April release is not extra runway
On December 20 the PSF published Python 2 series to be retired by April 2020, along with a matching press release. It says the last 2.7 release will come out in April 2020, that all development on Python 2 will then cease, and it urges users to migrate to avoid potential security vulnerabilities after April 2020.
If you only read that headline, you could come away thinking you have until April. I don’t think that’s the right reading. Put it next to the sunset page and PEP 373 and the picture is a code freeze in January with a packaging release a few months later. My take is to treat January 1 as the date fixes stop and April as the date the last tarball ships, and not to plan any security posture around the gap between them.
Find everywhere Python 2 still runs
The hard part of most migrations is the code you forgot, so start with a generous inventory. Look at application code, but also at crontabs, systemd units, CI scripts, Makefiles, packaging and release tooling, and anything with a #!/usr/bin/env python or #!/usr/bin/python shebang, because on many systems that unversioned name still means 2.7.
Then look at the operating systems underneath. Canonical’s October post on Python 2 and ROS notes that every Ubuntu release before 19.10 ships Python 2 in main, and that it was demoted to universe in 19.10. Red Hat’s position on RHEL 8, from Petr Viktorin’s Python in RHEL 8 post, is that Python 3.6 is the default, Python 2 is a separate python2 package, and there is no plain python command by default at all. Apple’s macOS Catalina release notes say Python 2.7 is only included for compatibility with legacy software and that future versions of macOS won’t include it. So the same script can get 2.7 on one box, fail with “command not found” on another, and lose its interpreter on a laptop after a future OS upgrade.
It’s also worth reading what the sunset page says about software you didn’t write: if you buy it or pay someone to run it, ask them. A vendor appliance with Python 2 inside it is still your risk.
Check your dependencies
Once you know where Python 2 lives, find out what’s blocking each piece. The official Porting Python 2 Code to Python 3 HOWTO in the Python docs recommends caniusepython3, which checks your dependencies directly and indirectly and reports which ones are blocking Python 3. It can also be wired into a test so the test starts failing once nothing is blocking you anymore.
For anything it flags, go to the project’s own docs, changelog, and PyPI trove classifiers rather than guessing. Debian’s Python maintainers put the trend bluntly in a debian-devel-announce message last month: more and more module authors don’t support Python 2 in their current or future releases. That cuts both ways. The newest release of a library is likely to support Python 3, but pinning an old release so you can stay on 2.7 probably means sitting on a version that isn’t getting fixes either.
Port with tests running on both versions
The porting HOWTO’s approach is to get your code running on Python 2.7 and Python 3 from the same source, which lets you move over gradually and drop 2.7 later. It starts with test coverage, suggesting coverage.py and a target somewhere above 80 percent, so that when a tool rewrites your code, a failure points at the rewrite and not at code you never tested. From there it recommends Futurize or Modernize to do the mechanical changes, Pylint’s --py3k check and Python 2’s -3 flag to catch regressions, and tox in CI so the suite runs under both interpreters on every change. It also suggests running a static type checker like mypy or pytype once as Python 2 and once as Python 3.
The most famous change is that print became a function. What’s New In Python 3.0 covers it along with the other common stumbling blocks. A __future__ import gives you the Python 3 behavior on 2.7, and the HOWTO suggests putting it at the top of new modules along with division and absolute_import:
from __future__ import print_function
# Python 2 only, a SyntaxError on Python 3:
# print "Processed", count, "rows"
print("Processed", count, "rows") # works on 2.7 and 3
Print is noisy but easy, and the tools handle it. The change that actually bites is text versus bytes.
Text versus bytes is where the real work is
In Python 2, str held both text and binary data, and Python would quietly coerce between byte strings and unicode when you mixed them. Python 3 splits them into str for text and bytes for binary data and doesn’t convert between them for you. The HOWTO is upfront that this is the part tools can’t fully automate, because you have to decide which of your APIs take text and which take bytes.
Its advice is to decode bytes to text as soon as they come in, encode text to bytes as late as possible on the way out, mark literals with b or u prefixes, and open files with io.open and an explicit binary mode where you mean binary. It also calls out a subtle one, indexing into bytes:
payload = b"123"
payload[1] # Python 2: '2' Python 3: 50
payload[1:2] # b'2' on both
text = payload.decode("utf-8") # decode at the edge
"id=" + text # fine on both
"id=" + payload # TypeError on Python 3
That last line is the kind of thing that blows up on Python 3 in a code path your tests didn’t cover. The HOWTO suggests running your Python 3 tests with -bb, which turns comparisons between bytes and str (and, from Python 3.5, between bytes and int) into exceptions instead of silently returning False.
Decide what “unsupported” means for you
After January 1, “Python 2 is unsupported” is true upstream, but some vendors have said they’ll keep patching their own packages. Canonical’s post says Python 2 from the Ubuntu 16.04 and 18.04 repositories will stay supported, with security updates, for the life of those releases regardless of upstream status. Red Hat’s Application Streams life cycle page lists the Python 2.7 stream in RHEL 8 as supported until June 2024. And on the other side, that same Debian announcement says they’re aiming to remove Python 2 for the bullseye release, or at least as much of it as possible.
That vendor support covers the interpreter and the packages that vendor ships. It doesn’t cover the pip installed libraries in your virtualenv, and it doesn’t cover your own code. So I’d frame the decision as a risk call per system. An internal script on an LTS box with vendor patched Python 2 and no third party dependencies can probably wait a bit. An internet facing app with a pile of pinned libraries that no longer get releases shouldn’t.
Where I’d start this week
If you only have a few hours before the holidays, spend them on the inventory. Grep your repos and servers for Python 2 shebangs and python calls in cron and CI, write down which OS and which vendor is behind each one, and run caniusepython3 against the projects that matter. That list turns “we should migrate someday” into specific work, and shows which systems are truly on their own after January 1.