Python 2 to 3 Migration: A Practical Plan for Code You Can’t Just Rewrite

    Matt Watson
    By Matt Watson · CEO of Full Scale, 4x Founder, Author of Product Driven
    6 min read
    python 2 to 3 migration hero
    In this article

    If you still have Python 2 in production, you already know the uncomfortable part: Python 2 reached end of life on January 1, 2020, which means no more security patches, no more fixes, and a growing list of libraries that have dropped support for it entirely. A Python 2 to 3 migration isn’t a nice-to-have anymore. It’s a security and hiring problem that compounds every year you leave it. The good news is that this is one of the more well-trodden migrations in software, and done right it’s far less scary than the codebase makes it feel.

    I run Full Scale, and we staff the engineers who do this kind of modernization work. Here’s the honest version of how a Python 2 to 3 migration actually goes, what breaks, and how to do it without stopping the business.

    Why this migration can’t keep waiting

    The case for moving is straightforward and getting stronger:

    • No security support. Python 2 stopped receiving patches in 2020. Every vulnerability discovered since then is unfixed in your runtime.
    • The library world has moved on. Major packages dropped Python 2 support years ago, so you’re frozen on old versions of your dependencies, which have their own unpatched flaws.
    • You can’t hire for it. New engineers don’t want to work in a dead language version, and the people who know Python 2 deeply are getting harder and more expensive to find.
    • The gap keeps widening. Python 3 has had years of performance and language improvements you simply can’t use. The longer you wait, the larger the jump becomes.

    This is the rare modernization where doing nothing is clearly the most expensive option. The question is never whether to migrate, only how.

    What actually breaks between Python 2 and 3

    Most of a Python 2 codebase converts more cleanly than people expect. The changes that bite are a known set:

    • Strings and bytes. This is the big one. Python 3 draws a hard line between text and bytes, and Python 2 code that played fast and loose with the two is where most of the real work lives, especially anything touching files, networking, or encodings.
    • print as a function. Mechanical, easy, and handled almost entirely by automated tooling.
    • Integer division. / changed behavior, and the silent ones are dangerous because the code still runs, just wrong.
    • Dictionary methods and iterators. dict.keys() and friends return views instead of lists, and a lot of code quietly assumed the old behavior.
    • Library and import changes. Renamed standard-library modules and third-party packages that need version bumps or replacements.

    The mechanical changes are not the risk. The risk is the behavioral ones that still run without error and produce subtly wrong results, which is exactly why this migration lives or dies on test coverage.

    The Python 2 to 3 migration plan that works: build a test safety net, reach a single Python 2/3-compatible codebase, convert module by module, then drop the bridge code.

    The plan that works

    A Python 2 to 3 migration succeeds when you treat it as an incremental, test-driven project, not a big-bang rewrite. The shape that works:

    1. Get a test safety net first. Before changing anything, build or shore up automated tests around the behavior you can’t afford to break. This is the single highest-value step, and skipping it is the most common reason these migrations go sideways.
    2. Reach a single Python 2/3-compatible codebase. Tools like python-future, six, and 2to3 let you get the code running under both versions at once. That means you migrate without a hard cutover, and you can ship continuously the whole way.
    3. Convert and verify in slices. Move module by module, leaning on the test suite to catch the silent behavioral changes, especially around strings and bytes.
    4. Flip the default and drop the compatibility layer. Once everything runs cleanly on Python 3, make it the only target and remove the bridging code.

    This is the Strangler pattern applied to a language migration: the system keeps running the entire time, and you’re never one risky deployment away from disaster. We go deeper on choosing between this kind of careful conversion and a clean rebuild in refactor vs rewrite.

    How AI changes a Python 2 to 3 migration

    This is where 2026 looks different from 2020. AI is genuinely good at the mechanical half of this migration: reading old Python 2 code, explaining what it does, flagging the string-versus-bytes hot spots, and proposing the Python 3 version. For a migration that’s mostly pattern-matching against a known set of changes, that’s a real time-saver, and it cuts the tedious part of the work down hard.

    Need senior Python engineers?

    Add vetted Python developers to your team for product, data, or backend work — staffed in about two weeks.

    The limit is the same as everywhere else in modernization. AI-generated code carries real bug and security risk, and the dangerous changes in this migration are precisely the silent behavioral ones an automated tool can miss. So AI accelerates the conversion, but a senior engineer still owns the test strategy and reviews the edge cases. The fuller set of ways these projects go wrong is in application modernization challenges.

    Who should do the work

    A Python 2 to 3 migration is a great fit for adding experienced engineers to your team rather than handing it to a black-box vendor, because the hard part is understanding your specific codebase’s assumptions, and that knowledge needs to stay with you afterward. Full Scale places senior Python developers who join your repo and your standups and do the migration alongside your own team, so the Python 3 expertise is in-house when the work is done. If you’re also staffing up the Python side more broadly, our Python developer job description is a good starting point for what to hire for.

    Frequently asked questions

    Is Python 2 still supported?

    No. Python 2 reached end of life on January 1, 2020, and receives no security patches or fixes. Running it in production means any vulnerability found since then is unpatched, and a growing share of libraries no longer support it at all.

    How long does a Python 2 to 3 migration take?

    It depends entirely on the size of the codebase and the quality of its existing tests. A small, well-tested service can move in weeks; a large application with thin test coverage takes longer, because the first real task is building the safety net that lets you migrate without breaking behavior. The codebase’s reliance on the string-versus-bytes distinction is usually the biggest factor.

    What is the hardest part of migrating Python 2 to 3?

    The string-and-bytes split. Python 3 enforces a hard distinction between text and binary data that Python 2 blurred, and code touching files, networks, or encodings is where most of the genuine work lives. Mechanical changes like print are easy; the silent behavioral changes that still run but produce wrong results are the real risk.

    Can you migrate Python 2 to 3 incrementally?

    Yes, and you should. Using tools like python-future and six, you can get the code running under both Python 2 and 3 at once, then convert module by module while shipping continuously, rather than attempting a single risky cutover. This keeps the system running throughout the migration.

    Can AI do a Python 2 to 3 migration?

    AI handles the mechanical conversion well and speeds up the tedious parts significantly, but it can miss the silent behavioral changes that make this migration risky. A senior engineer still needs to own the test strategy and review the edge cases, since AI-generated code carries its own bug and security risk.

    The longer you wait, the bigger the jump

    A Python 2 to 3 migration is not the terrifying project an aging codebase makes it feel like. It’s a well-understood, incremental, test-driven piece of work, and the only thing that gets worse with time is the cost of putting it off. Build the test net, get to a dual-compatible codebase, convert in slices, and you keep the business running the whole way.

    If you’d rather do it with engineers who’ve migrated production Python before, that’s what we do. Talk to us about your migration, and we’ll tell you honestly what it’ll take.

    Get Product-Driven Insights

    Weekly insights on building better software teams, scaling products, and the future of offshore development.

    Subscribe on Substack

    Ready to add senior engineers to your team?

    Book a 15-minute call. Tell us your stack and where the gaps are, and we'll show you the engineers we'd put on your team.