# Python 2 to Python 3 migration: a practical checklist

URL: https://mechaneforge.tech/blog/python-2-to-3-migration-checklist/
Published: 2026-10-15
By: Mechane Forge

> 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?

1. Inventory every Python 2 script, service and cron job, including the ones nobody remembers.
2. Add tests around current behaviour before touching code. Even a handful of end-to-end checks helps.
3. Upgrade to the last Python 2.7 and pin dependencies, so changes happen one at a time.
4. Run compatibility tooling such as pyupgrade, futurize or modernize, and static checks with mypy, to find incompatible code.
5. Fix text and bytes handling, the biggest source of bugs, especially around files, networks and databases.
6. Replace or upgrade dependencies that do not support Python 3.
7. Run old and new side by side on real data and compare outputs before switching.
8. Cut 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 |
| print | 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](https://mechaneforge.tech/services/legacy-modernization) service starts by documenting behaviour from the running system.
