Python 2 to 3 Migration & Modernization

ModernLift · ·8 min read

Python 2.7 reached end of life on January 1, 2020 — the Python core team stopped all work on it, including security fixes, and the final 2.7.18 release in April 2020 only bundled changes already made before the sunset. Migrating a Python 2 application to Python 3 means moving the code, its dependencies, and the behaviors that changed between the versions onto a supported runtime, ideally slice by slice with each proven equivalent before cutover.

A Python 2 application is rarely just Python 2. It’s a requirements.txt full of libraries that stopped shipping Python 2 wheels years ago, a body of code that assumes strings are bytes, a set of C extensions built against an old interpreter, and behavior the team relies on without realizing it changed in Python 3. That’s why “run 2to3 and ship it” is never the whole job — and why staying on Python 2 is a problem that quietly gets worse, not one that holds steady.

The one date that settles everything for Python 2

Unlike a product with a rolling lifecycle, Python 2 has one date that settles everything:

MilestoneDateStatus
Python 2.7 end of life (sunset)January 1, 2020End of life — no fixes, ever
Final 2.7.18 releaseApril 2020Pre-sunset fixes only — not support
Python 3SupportedThe target runtime

Dates are from PEP 373 and the Python Software Foundation’s sunset announcement. The sunset was deliberate and final: as of January 1, 2020 the core team stopped improving Python 2 at all, explicitly including the case where someone finds a security problem in it. The 2.7.18 release a few months later only bundled fixes already made before the deadline — a courtesy, not a reopening of support. The line that matters is absolute: no fix of any kind will ever ship for Python 2 again. An application still on it isn’t “old but fine” — it’s running on a runtime its maintainers have permanently walked away from.

What end of life actually means for a Python 2 app

Python 2’s hard sunset raises the stakes over an ordinary version-behind situation, for three reasons.

  • The ecosystem left, not just the core. It isn’t only CPython that stopped — the major libraries, frameworks, and even package installers have dropped Python 2. The application’s dependencies are decaying independently, and each one that drops support narrows the path further.
  • It collides with compliance. PCI DSS, SOC 2, and HIPAA all expect runtimes in scope to be patchable. An unsupported Python 2 in a regulated data path is a finding waiting to be written, with no patch line to point to at all.
  • It gets harder every year. Unlike a stable old version, a Python 2 app is rotting at the edges — newer OS images drop the interpreter, build tooling breaks, and the migration that was merely large last year is larger now.

The migration options

There is no single right move; there’s a right move for your application, and it turns on how big the codebase is and what its dependencies require.

  • Port in place to Python 3 where the dependencies have clean Python 3 releases. Automated tooling handles the mechanical syntax, and the work concentrates on the semantic changes — text versus bytes, integer division, library moves. The cheapest path when the libraries came along.
  • Upgrade the dependency stack alongside the runtime. When a key library never made the Python 3 jump or changed dramatically, that dependency has to be replaced or re-implemented as part of the move — more involved, because the change isn’t only in your code.
  • Re-platform the parts that fight the move. Where C extensions, heavy reliance on Python 2-only behavior, or an abandoned framework block a straight port, those slices need real re-implementation. The bulk of the genuine work lives here.

The decision is rarely about the syntax — it’s about the dependencies and the semantic behaviors the code relies on. That coupling is what turns a 2-to-3 port into a modernization project.

Porting to Python 3 without freezing the app

We treat the application the same way we treat any legacy system: not a single risky cutover, but a sequence of small, reversible steps.

A strangler facade lets the legacy Python 2 application and the Python 3 path run side by side. We break the application into slices — a module, a service, a bounded workflow — and before any slice carries live work, we prove it behaves identically to the legacy: same results, same outputs, reconciled against the original. AI-accelerated discovery reads the code, the dependencies, and the C-extension assumptions end to end and captures what the application actually does — including the undocumented logic the original authors never wrote down, and the version-specific behaviors that are easy to miss — under senior-engineer review. Work shifts only on green, rollback stays a flag away, and the Python 2 path is retired only once nothing depends on it.

When your own team can run the port

Not every Python 2 port needs a partner, and we’ll say so. A small script or a self-contained service whose dependencies all have clean Python 3 releases is often a job a capable in-house team can run with automated tooling and a good test suite — and we’ll tell you that on the call rather than manufacture a project. The slice-by-slice approach earns its place when the codebase is large, when a critical dependency never made the jump, when C extensions or Python 2-only behavior block a straight port, or when the application can’t take downtime. Matching the effort to the actual difficulty is part of the assessment, not an afterthought.

Where to start

The first step is small and bounded: understand the codebase before committing to a path. A discovery call scopes how large the Python 2 surface is, which dependencies still lack a Python 3 path, where the compliance exposure sits, and whether the right move is a straightforward port or a deeper migration — on evidence, not a sales pitch. Reach the team at sales@modernlift.ai.

Frequently asked questions

When did Python 2 reach end of life?
Python 2.7 reached end of life on January 1, 2020, the official sunset date set by the Python Software Foundation. After that date the core team made no further bug fixes, changes, or security patches. A final 2.7.18 release shipped in April 2020, but it only bundled fixes already completed before the sunset — it did not extend support.
Why can't we just keep running Python 2?
Because nothing about it is maintained anymore. No security vulnerability found in Python 2 since January 2020 has been or will be fixed by the core team, the major libraries have dropped Python 2 support, and package tooling increasingly refuses to install on it. The longer an application stays, the more its dependencies rot out from under it and the harder the eventual move becomes.
How hard is a Python 2 to 3 migration?
It varies with the codebase. Automated tooling handles the mechanical syntax changes, but the real work is in behaviors that changed semantically — text versus bytes handling, integer division, dictionary ordering, and library APIs that moved or disappeared. Done incrementally, each module is migrated and proven to behave identically before cutover, rather than flipping the whole application at once.
Who provides Python 2 to 3 migration services?
We do — Python 2 to 3 migration and modernization services are part of what we do. We move a Python 2 application onto Python 3 slice by slice, bringing the dependencies along and proving each module equivalent to the legacy before it carries live work. We sell the migration itself, not licenses or tools, and we'll tell you when a small codebase is a job your own team can run with tooling and a good test suite rather than manufacture a project.
How much does a Python 2 to 3 migration cost?
It scales with the codebase, not a list price. The drivers are how large the Python 2 surface is, how many slices it breaks into, whether key dependencies ever made the Python 3 jump, the C extensions and Python 2-only behavior involved, and how much parity has to be proven before each cutover. Our [legacy cost calculator](/legacy-cost-calculator) gives a directional estimate, and a [discovery call](/meet) scopes the real number against your codebase.
How do I choose a Python modernization partner?
Judge a partner on how they de-risk the move, not how fast they promise to finish. Look for incremental, slice-by-slice migration over a single risky cutover; proof that each module behaves identically to the legacy before work shifts; senior-engineer review of AI-accelerated discovery so the version-specific behaviors aren't missed; and the honesty to say when an in-place port is a job you can run yourselves. Matching the effort to the actual difficulty should be part of the assessment, not an upsell.