Python 2 to Python 3 migration: a practical checklist
Python 2 reached end of life on 1 January 2020, but many business systems still run on it. Here is a safe, step-by-step path to Python 3.
Python 2 stopped receiving updates on 1 January 2020, so code still running on it gets no security fixes. Migrate safely by adding tests first, upgrading dependencies, running both versions side by side with compatibility tools, fixing text and bytes handling, then switching over and removing the compatibility layer.
Python 2.7 was retired by the Python Software Foundation on 1 January 2020. Plenty of internal tools, scrapers, report generators and back-office scripts still run on it. They keep working, until a library, an operating system or an auditor forces the issue. Migrating on your own timetable is far cheaper than doing it in an emergency.
Why does Python 2 still matter?
- No security patches: vulnerabilities found in the interpreter are not fixed.
- Libraries have dropped support, so you cannot upgrade dependencies that contain fixes.
- Modern operating systems and containers increasingly omit Python 2.
- Hiring and maintaining is harder every year.
What is a safe migration order?
- 01Inventory every Python 2 script, service and cron job, including the ones nobody remembers.
- 02Add tests around current behaviour before touching code. Even a handful of end-to-end checks helps.
- 03Upgrade to the last Python 2.7 and pin dependencies, so changes happen one at a time.
- 04Run compatibility tooling such as pyupgrade, futurize or modernize, and static checks with mypy, to find incompatible code.
- 05Fix text and bytes handling, the biggest source of bugs, especially around files, networks and databases.
- 06Replace or upgrade dependencies that do not support Python 3.
- 07Run old and new side by side on real data and compare outputs before switching.
- 08Cut over, then remove the compatibility layer and update CI and deployment.
Which changes cause the most trouble?
| Area | Python 2 behaviour | What to check in Python 3 |
|---|---|---|
| Strings | str is bytes, unicode is text | str is text, bytes is separate; encode and decode at the edges |
| Division | 5 / 2 equals 2 | 5 / 2 equals 2.5; use // for integer division |
| A statement | A function | |
| Dictionaries | keys() returns a list | Returns a view; wrap in list() when needed |
| Imports | Implicit relative imports | Absolute imports only |
Should you rewrite instead?
For small scripts, a rewrite can be faster. For systems with years of embedded business rules, migrate incrementally and keep the tests. If the source is lost or poorly understood, our legacy modernization service starts by documenting behaviour from the running system.