How to Migrate from Python 2 to Python 3

Type: Software Reference Confidence: 0.93 Sources: 8 Verified: 2026-02-23 Freshness: monthly

TL;DR

Constraints

Quick Reference

Python 2 PatternPython 3 EquivalentExample
print "hello"print("hello")from __future__ import print_function enables Py2 compat [src2]
except Exception, e:except Exception as e:2to3 except fixer handles this automatically [src2]
dict.iteritems()dict.items() (returns view)for k, v in d.items(): works in both with future [src1]
dict.has_key(key)key in dict2to3 has_key fixer handles this [src2]
unicode("text")str("text")In Py3, all strings are Unicode by default [src5]
raw_input()input()Py2 input() did eval() — use raw_input() equivalent [src2]
xrange(10)range(10)Py3 range returns iterator like Py2 xrange [src2]
5 / 2 == 2 (floor)5 / 2 == 2.5 (true div)from __future__ import division or use // [src1]
raise ValueError, "msg"raise ValueError("msg")2to3 raise fixer handles this [src2]
long(42)int(42)long type merged into int in Py3 [src5]
map(fn, lst) returns listlist(map(fn, lst)) or comprehensionPy3 map returns iterator [src2]
filter(fn, lst) returns listlist(filter(fn, lst)) or comprehensionPy3 filter returns iterator [src2]
from StringIO import StringIOfrom io import StringIOStandard library reorganized [src2]
__metaclass__ = Metaclass X(metaclass=Meta):2to3 metaclass fixer handles this [src2]
b'data'[0] == b'd'b'data'[0] == 100 (int)Py3 bytes indexing returns int, not bytes [src1]

Decision Tree

START
├── Is the codebase still running Python 2.6 or older?
│   ├── YES → First upgrade to Python 2.7 (required baseline for all tools)
│   └── NO ↓
├── Do you need to support both Python 2 AND 3 simultaneously?
│   ├── YES → Use futurize or python-modernize + six for dual-compatible code
│   └── NO ↓
├── Is the codebase <10,000 lines with good test coverage?
│   ├── YES → Run futurize for one-shot conversion, fix remaining issues manually
│   └── NO ↓
├── Is the codebase >100,000 lines or mission-critical?
│   ├── YES → Incremental migration: futurize stage1 first, then stage2, test each module
│   └── NO ↓
├── Does the code heavily handle binary data (network, files, crypto)?
│   ├── YES → Focus on unicode/bytes boundaries first; use type annotations + mypy
│   └── NO ↓
├── Are you targeting Python 3.13+ (where 2to3 is removed)?
│   ├── YES → Use futurize or LibCST — do NOT depend on 2to3
│   └── NO ↓
└── DEFAULT → futurize --stage1 --stage2 -w, then fix test failures one by one

Step-by-Step Guide

1. Ensure Python 2.7 baseline and add test coverage

All migration tools require Python 2.7 as the minimum source version. Before touching any code, ensure your test suite covers at least 80% of the codebase. [src1]

# Install coverage tool
pip install coverage

# Run tests with coverage
coverage run -m pytest tests/
coverage report --show-missing

# Target: 80%+ coverage before migration

Verify: coverage report shows >= 80% coverage on critical modules.

2. Add __future__ imports to prevent regressions

Add forward-compatible imports to every Python file. This makes Python 2 behave more like Python 3, catching issues early. [src1]

# Add to the top of EVERY .py file
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals  # optional, see Caveats

Verify: python2 -3 -Werror your_script.py — runs under Python 2 but raises errors for Python 3 incompatibilities.

3. Run futurize stage 1 (safe, mechanical fixes)

Stage 1 applies only safe, non-controversial transformations: print function, except syntax, dict methods, etc. [src7]

# Install python-future
pip install future

# Run stage 1 (safe fixes only)
futurize --stage1 -w mypackage/

# Review changes
git diff

Verify: python2 -m pytest tests/ — all tests still pass under Python 2 after stage 1 changes.

4. Run futurize stage 2 (Python 3 idioms)

Stage 2 adds future library imports to backport Python 3 types and builtins to Python 2. [src7]

# Run stage 2
futurize --stage2 -w mypackage/

# Review and test
git diff
python2 -m pytest tests/

Verify: python2 -m pytest tests/ and python3 -m pytest tests/ both pass.

5. Fix unicode/bytes boundaries manually

Automated tools cannot determine developer intent for string types. Audit every I/O boundary: file reads, network sockets, database queries, API responses. [src1, src3]

# BEFORE: ambiguous in Python 2
data = open("config.txt").read()  # str (bytes) in Py2, str (unicode) in Py3

# AFTER: explicit encoding
with open("config.txt", "r", encoding="utf-8") as f:
    text = f.read()  # Always str (unicode)

# Binary files must use 'rb'
with open("image.png", "rb") as f:
    raw_bytes = f.read()  # Always bytes

Verify: python3 -bb -m pytest tests/ — the -bb flag raises BytesWarning as errors when bytes and str are compared or concatenated.

6. Update dependencies and standard library imports

Check that all third-party packages support Python 3. Replace renamed standard library modules. [src1, src4]

# Check dependency compatibility
pip install caniusepython3
caniusepython3 --requirements requirements.txt

# Common import changes (2to3 handles most):
# import ConfigParser   ->  import configparser
# import Queue          ->  import queue
# from StringIO import StringIO -> from io import StringIO
# import urllib2         ->  import urllib.request
# import urlparse        ->  import urllib.parse
# import cPickle         ->  import pickle

Verify: python3 -c "import mypackage" succeeds without ImportError.

7. Run the full test suite under Python 3

Use tox or CI to test across multiple Python versions. Fix remaining failures one by one. [src1, src3]

# Install tox
pip install tox

# tox.ini configuration:
# [tox]
# envlist = py38, py39, py310, py311, py312, py313
# [testenv]
# deps = pytest
# commands = pytest tests/

tox

Verify: tox reports all environments passing. Zero test failures across all target Python versions.

8. Remove Python 2 compatibility code and drop support

Once all tests pass on Python 3 and you have confirmed production stability, clean up compatibility shims. [src3, src4]

# Remove __future__ imports (no longer needed)
# Remove six/future imports
# Remove version-checking conditionals
# Update setup.py classifiers:
#   'Programming Language :: Python :: 3 :: Only'

pip uninstall six future
grep -rn "__future__\|import six\|from future" mypackage/

Verify: No Python 2 compatibility code remains. App runs cleanly on Python 3 only.

Code Examples

Python: Fixing print, division, and string handling

# Input:  Python 2 code with common patterns
# Output: Python 3 compatible code

# --- PYTHON 2 (BEFORE) ---
# print "Processing %d items" % count
# result = total / count  # floor division
# name = u"Muller"
# data = open("file.txt").read()
# for key, val in config.iteritems():
#     print key, val

# --- PYTHON 3 (AFTER) ---
print("Processing {} items".format(count))       # print is a function
result = total / count       # true division (returns float)
result_int = total // count  # floor division (explicit)
name = "Muller"             # all strings are Unicode
with open("file.txt", "r", encoding="utf-8") as f:
    data = f.read()          # explicit encoding
for key, val in config.items():  # .items() returns view
    print(key, val)

Python: Migrating exception handling and class definitions

# Input:  Python 2 exception and class patterns
# Output: Python 3 equivalents

from abc import ABCMeta

# BEFORE (Python 2):
# class OldStyle:
#     __metaclass__ = ABCMeta
#     def method(self):
#         try:
#             risky()
#         except IOError, e:
#             raise RuntimeError, "Failed: " + str(e)

# AFTER (Python 3):
class NewStyle(metaclass=ABCMeta):  # metaclass as keyword arg
    def method(self):
        try:
            risky()
        except IOError as e:  # 'as' syntax required
            raise RuntimeError("Failed: " + str(e)) from e  # chained exceptions
            # Note: exception variable 'e' is deleted after except block in Py3

Bash: Automated migration with futurize

# Input:  A Python 2 project directory
# Output: Python 2/3 compatible codebase

# Step 1: Install tools
pip install future pylint tox coverage

# Step 2: Stage 1 conversion (safe mechanical fixes)
futurize --stage1 -w src/

# Step 3: Run tests to verify no regressions
python2 -m pytest tests/ -v

# Step 4: Stage 2 conversion (Python 3 idioms with future imports)
futurize --stage2 -w src/

# Step 5: Test under both interpreters
python2 -m pytest tests/ -v
python3 -bb -m pytest tests/ -v

# Step 6: Check for remaining Py2-only patterns
pylint --py3k src/

# Step 7: Verify with type checking (optional but recommended)
pip install mypy
mypy --python-version 3.12 src/

Python: Handling bytes/str boundary correctly

# Input:  Code that processes both text and binary data
# Output: Python 3 compatible version with explicit type handling

import json
import hashlib

def process_api_response(raw_response: bytes) -> dict:
    """Decode bytes from network, process as text, return parsed data."""
    # Decode bytes to str at the boundary
    text = raw_response.decode("utf-8")

    # Work with text (str) internally
    parsed = json.loads(text)
    name = parsed["name"]  # str in Python 3

    # Encode back to bytes only when needed for binary operations
    name_hash = hashlib.sha256(name.encode("utf-8")).hexdigest()
    parsed["name_hash"] = name_hash
    return parsed

def read_mixed_file(text_path: str, binary_path: str):
    """Demonstrate correct file I/O for text vs binary."""
    # Text files: always specify encoding
    with open(text_path, "r", encoding="utf-8") as f:
        lines = f.readlines()  # List[str]

    # Binary files: use 'rb' mode
    with open(binary_path, "rb") as f:
        data = f.read()  # bytes

    # CAUTION: bytes indexing returns int in Python 3
    first_byte = data[0]  # int, e.g., 137 for PNG header
    # NOT a single-byte string like in Python 2

    return lines, data

Python: Using LibCST for modern code transformation (Python 3.13+)

# Input:  Python 2-style code patterns to modernize
# Output: Automated refactoring using LibCST (replaces 2to3 on Python 3.13+)

# Install: pip install libcst
import libcst as cst

# LibCST parses code as a concrete syntax tree (preserves formatting)
# Use it to build custom codemods for patterns futurize does not cover
source = 'msg = "Hello %s, you have %d items" % (name, count)'
tree = cst.parse_module(source)
# Apply codemod transformations via libcst.codemod framework
# See: https://libcst.readthedocs.io/en/latest/codemods_tutorial.html

Anti-Patterns

Wrong: Using bare except with comma syntax

# ❌ BAD — Python 2 exception syntax fails in Python 3
try:
    result = int(user_input)
except ValueError, e:
    print "Invalid input:", e

Correct: Use 'as' keyword and print function

# ✅ GOOD — Works in both Python 2.7+ and Python 3
from __future__ import print_function
try:
    result = int(user_input)
except ValueError as e:
    print("Invalid input:", e)

Wrong: Relying on implicit str/bytes mixing

# ❌ BAD — Works in Python 2 but raises TypeError in Python 3
def build_header(name, value):
    return b"Header: " + name + b"=" + value  # name/value might be str

Correct: Explicitly encode/decode at boundaries

# ✅ GOOD — Explicit bytes handling for Python 3
def build_header(name: str, value: str) -> bytes:
    header_str = "Header: {}={}".format(name, value)
    return header_str.encode("utf-8")

Wrong: Using dict.has_key() and iteritems()

# ❌ BAD — has_key() and iteritems() removed in Python 3
if config.has_key("timeout"):
    for key, val in config.iteritems():
        process(key, val)

Correct: Use 'in' operator and .items()

# ✅ GOOD — Works in both Python 2.7 and Python 3
if "timeout" in config:
    for key, val in config.items():
        process(key, val)

Wrong: Assuming division returns integer

# ❌ BAD — Returns 2 in Python 2, returns 2.5 in Python 3
def calculate_average(total, count):
    return total / count  # Unexpected float in Python 3

Correct: Use explicit floor division or true division

# ✅ GOOD — Explicit about intent
from __future__ import division

def calculate_average(total, count):
    return total / count    # Always returns float (true division)

def calculate_pages(total, per_page):
    return total // per_page  # Always returns int (floor division)

Wrong: Version-checking with equality

# ❌ BAD — Breaks if Python 4 ever exists
import sys
if sys.version_info[0] == 3:
    from configparser import ConfigParser
else:
    from ConfigParser import ConfigParser

Correct: Use feature detection (try/except)

# ✅ GOOD — Feature detection is future-proof
try:
    from configparser import ConfigParser
except ImportError:
    from ConfigParser import ConfigParser

Wrong: Using 2to3 on Python 3.13+

# ❌ BAD — 2to3 was removed in Python 3.13. This command will fail.
# $ 2to3 -w mypackage/
# ModuleNotFoundError: No module named 'lib2to3'

Correct: Use futurize or LibCST instead

# ✅ GOOD — futurize works on all Python versions
pip install future
futurize --stage1 --stage2 -w mypackage/

# Or use LibCST for advanced transformations on Python 3.13+
pip install libcst
python -m libcst.tool codemod mypackage/

Common Pitfalls

Diagnostic Commands

# Check current Python version
python --version && python3 --version

# Run Python 2 with Python 3 deprecation warnings
python2 -3 -Werror script.py

# Run Python 3 with bytes/str mixing detection
python3 -bb -m pytest tests/ -v

# Check if dependencies support Python 3
pip install caniusepython3
caniusepython3 --requirements requirements.txt

# Run pylint with Python 3 compatibility checker
pylint --py3k mypackage/

# Count Python 2-specific patterns remaining
grep -rn "print " --include="*.py" | grep -v "print(" | wc -l
grep -rn "has_key\|iteritems\|itervalues\|iterkeys" --include="*.py" | wc -l
grep -rn "except.*," --include="*.py" | grep -v "except.*as" | wc -l

# Test across multiple Python versions with tox
tox -e py38,py39,py310,py311,py312,py313

# Run futurize in dry-run mode (no changes)
futurize --stage1 mypackage/ 2>&1 | head -50

# Check for __future__ imports coverage
grep -rL "from __future__ import" --include="*.py" mypackage/

# Verify type annotations with mypy
mypy --python-version 3.12 mypackage/

Version History & Compatibility

VersionStatusBreaking ChangesMigration Notes
Python 3.14 (Oct 2025)Currentfrom __future__ import annotations deprecated (PEP 649); incremental GCDeferred annotation evaluation is now native; template strings added
Python 3.13 (Oct 2024)Stable2to3 / lib2to3 removed; PEP 594 removes 20 stdlib modulesUse futurize or LibCST instead; check for removed modules
Python 3.12 (Oct 2023)StableImproved error messages; 2to3 still available but deprecatedLast Python version with 2to3 in stdlib
Python 3.10Securitycollections.abc must be imported explicitlyfrom collections.abc import Mapping
Python 3.8-3.9EOLAssignment expressions (:=), positional-only paramsStable target for most migrations
Python 3.6-3.7EOLf-strings, dict insertion order guaranteed (3.7)Minimum recommended target for new migrations
Python 2.7EOL (Jan 2020)Last Python 2 release everRequired as migration source baseline

When to Use / When Not to Use

Use WhenDon't Use WhenUse Instead
Your codebase runs Python 2.7Code is already Python 3 onlyNo migration needed
Python 2 EOL means no security patchesEmbedded system with frozen Python 2 runtimeKeep Python 2, isolate from network
You need async/await, f-strings, type hintsThrowaway script you'll never run againLeave as-is
Third-party deps have dropped Py2 supportMigration blocked by a dep with no Py3 supportFork/replace the dependency first
Team hiring — Python 3 devs are the normLegacy system being decommissioned within 6 monthsRun out the clock on Python 2
Performance gains (12% CPU, 30% memory)Code is <100 lines and trivially rewriteableRewrite from scratch in Python 3

Important Caveats

Related Units