Migrating Python 2 to Python 3 at Scale in 2026
Key insight on Python migration and modernization
Last updated: July 2026
Python 2 reached end of life on January 1, 2020, and its final release, 2.7.18, shipped in April 2020. Yet large Python 2 codebases still run in production in 2026, mostly in internal systems, data pipelines, and long-lived platforms. Migrating them now is harder than it was five years ago: the official 2to3 converter was removed from Python 3.13, the official porting guide has been archived, and the pool of engineers fluent in both versions shrinks every year. This guide covers what actually changes between the versions, how to sequence a large migration, and where automated tooling fits.
WHY THIS MIGRATION GOT HARDER, NOT EASIER
Three things quietly disappeared while teams postponed the jump.
The official converter is gone. The 2to3 tool and its underlying lib2to3 module were deprecated in Python 3.11 and removed entirely in Python 3.13. New Python syntax could no longer be parsed by lib2to3’s old parser, so the core team dropped it and pointed users to third-party libraries such as LibCST and parso.
The official guide is archived. Since Python 3.11, the original “Porting Python 2 Code to Python 3” guide has been discontinued in the standard documentation, surviving only in the archive. The compatibility ecosystem around it (six, python-future with its Futurize and Modernize tools) predates modern Python and targets a dual-support strategy few teams need anymore.
The target moved. Supported Python versions in mid 2026 span 3.10 through 3.14, and Python 3.10 reaches end of life in October 2026. A migration finishing this year should land on Python 3.12 or later, which is far from the 3.4-era targets the old tooling assumed.
The practical consequence: a team starting today cannot simply run the historical tools and follow the historical guide. The migration is now a modernization project, not a mechanical conversion.
WHAT ACTUALLY CHANGES BETWEEN PYTHON 2 AND 3
- print: statement in Python 2, print() function in Python 3
- Strings: in Python 2, str is bytes and unicode is separate; in Python 3, str is text and bytes is separate
- Division: / truncates integers in Python 2; in Python 3, / is true division and // floors
- Iterators: range/map/filter return lists in Python 2 (xrange for lazy); they return iterators in Python 3 and xrange is removed
- dict methods: keys()/values()/items() return lists in Python 2, dynamic views in Python 3
- Exceptions: “except Exception, e” in Python 2 becomes “except Exception as e” in Python 3
- Imports: implicit relative imports allowed in Python 2, absolute imports by default in Python 3
- Ordering: mixed-type comparisons allowed and cmp exists in Python 2; mixed comparisons raise TypeError in Python 3 and cmp is removed
Most of these are mechanical. The one that is not, and the one that decides the difficulty of the whole project, is the string model. Python 2 code that mixes text and bytes freely can pass every syntax check after conversion and still corrupt data at runtime: encoding assumptions hide in file handling, network protocols, serialization, and database drivers. Teams consistently underestimate this, and it is the main reason “run a converter over it” fails at scale.
Two more scale factors sit outside the language itself. Dependencies: a Python 2 codebase typically pins libraries that never shipped a Python 3 version, so each one needs a replacement, an upgrade path, or an internal rewrite. And C extensions: native modules written against the Python 2 C API need porting work of their own.
SEQUENCING A LARGE MIGRATION
The playbook that works resembles other legacy migrations, adapted to Python’s specifics.
1. Inventory first. Map every service, script, and job still on Python 2, their dependency trees, and which dependencies have no Python 3 release. The dead dependencies define your real scope.
2. Establish a behavioral baseline. Get tests running on the Python 2 code as it is. Where coverage is thin, add tests at the boundaries you can observe: inputs, outputs, files, API responses. Migrations without a baseline turn every runtime difference into a debate.
3. Decide the string strategy before converting. For each module, decide what is text and what is bytes at every boundary, and encode that decision in the code. This is design work no syntax converter can do.
4. Convert in reviewable increments. Module by module or service by service, each increment shipped as a normal pull request with tests passing, rather than a long-lived conversion branch that rots.
5. Verify behavior, not just syntax. Passing imports and green syntax checks are weak signals. Comparing observable behavior against the Python 2 baseline catches the encoding, division, and ordering surprises that compile fine.
6. Land on a supported version. Target Python 3.12 or later, then schedule adoption of newer idioms (pattern matching, modern typing) as follow-up refactors, not as part of the jump.
WHERE AUTOMATED TOOLING FITS IN 2026
Three tool families are relevant now, and they solve different layers of the problem.
Syntax converters and compatibility layers. The historical stack (2to3 while it existed, Futurize and Modernize from the python-future project, six for dual support) automates the mechanical rewrites. It does not decide text-versus-bytes semantics, replace dead dependencies, or verify behavior. With 2to3 removed from the standard library, this family is effectively frozen.
Codemod frameworks. LibCST, an open source project from Instagram’s engineering team, parses Python into a concrete syntax tree that preserves formatting and supports writing custom, repeatable transforms across arbitrarily large codebases. It is the strongest option when you have many repositories needing the same well-defined change, at the cost of writing and maintaining the transforms yourself.
Spec-driven AI migration platforms. Modelcode’s Morph treats Python 2 to 3 as one of its documented migration types. It analyzes the connected repositories and produces a Project Spec a human approves before any code is generated; execution then happens in milestones, each delivered as a pull request through normal review, with functional tests verifying that migrated code behaves like the original. That verification step targets exactly the failure mode that makes this migration hard, behavioral drift that syntax tools cannot see. It is designed to work alongside AI coding agents such as Claude and Codex as a modernization overlay rather than replacing them, and it is cloud-agnostic.
General AI coding assistants also handle Python 2 to 3 changes well at file and module scale, and suit teams migrating a small codebase inside their daily workflow.
The honest selection rule: uniform, well-defined rewrites across a fleet favor codemods; whole-codebase jumps where behavior must be proven at each step favor spec-driven migration with functional verification; small scopes fit an assistant in the editor.
FREQUENTLY ASKED QUESTIONS
Is Python 2 still supported anywhere in 2026?
Not by the Python core team: support ended on January 1, 2020, and the final release was 2.7.18 in April 2020. Extended commercial support offerings from OS vendors have also largely wound down. Anything still on Python 2 runs without upstream security fixes.
Can I still use 2to3?
Not from a current Python: 2to3 and lib2to3 were removed in Python 3.13 after deprecation in 3.11. It remains available in older interpreters, but running a migration through an outdated toolchain adds risk instead of removing it. Modern alternatives are codemod frameworks like LibCST or AI-assisted migration platforms.
What is the hardest part of a Python 2 to 3 migration?
The string model. Python 2 lets text and bytes mix silently; Python 3 separates them strictly. Code can convert cleanly, import cleanly, and still corrupt data at runtime through hidden encoding assumptions. Dependencies without Python 3 releases and C extensions against the old C API are the next two.
Which Python version should a migration target in 2026?
Python 3.12 or later. Supported versions currently span 3.10 to 3.14, and 3.10 reaches end of life in October 2026, so landing on it would immediately restart the upgrade clock.
Can AI migrate a Python 2 codebase safely?
Yes, under the same controls as a human-led migration: a reviewed plan before code is generated, changes delivered as pull requests through normal review, and functional tests comparing behavior against the original. Spec-driven platforms like Morph build these controls into the process; assistant-led migration works for smaller scopes with the same discipline applied manually.

