futurize or python-modernize) to handle syntax changes, then manually fix unicode/bytes boundaries and test with python3 -bb to catch type-mixing bugs. [src1, src2]futurize --stage1 --stage2 -w mypackage/str (text) from bytes (binary), and code that mixes them will raise TypeError at runtime. [src1, src3]2to3 tool and lib2to3 module were deprecated in Python 3.11 and removed entirely in Python 3.13 (October 2024). Use futurize from python-future, python-modernize, or LibCST instead. [src2, src8]TypeError when concatenating or comparing str and bytes. Every I/O boundary (file, network, database) must explicitly .encode() or .decode(). [src1, src3]from __future__ import annotations deprecated in Python 3.14: PEP 649 (deferred evaluation of annotations) is now the standard mechanism. While the directive still works, plan for eventual removal. [src1]dict ordering if targeting Python 3.6 or earlier. Use collections.OrderedDict for guaranteed ordering on older targets. [src6]| Python 2 Pattern | Python 3 Equivalent | Example |
|---|---|---|
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 dict | 2to3 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 list | list(map(fn, lst)) or comprehension | Py3 map returns iterator [src2] |
filter(fn, lst) returns list | list(filter(fn, lst)) or comprehension | Py3 filter returns iterator [src2] |
from StringIO import StringIO | from io import StringIO | Standard library reorganized [src2] |
__metaclass__ = Meta | class 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] |
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
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.
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.
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.
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.
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.
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.
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.
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.
# 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)
# 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
# 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/
# 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
# 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
# ❌ BAD — Python 2 exception syntax fails in Python 3
try:
result = int(user_input)
except ValueError, e:
print "Invalid input:", e
# ✅ 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)
# ❌ 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
# ✅ 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")
# ❌ BAD — has_key() and iteritems() removed in Python 3
if config.has_key("timeout"):
for key, val in config.iteritems():
process(key, val)
# ✅ GOOD — Works in both Python 2.7 and Python 3
if "timeout" in config:
for key, val in config.items():
process(key, val)
# ❌ 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
# ✅ 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)
# ❌ BAD — Breaks if Python 4 ever exists
import sys
if sys.version_info[0] == 3:
from configparser import ConfigParser
else:
from ConfigParser import ConfigParser
# ✅ GOOD — Feature detection is future-proof
try:
from configparser import ConfigParser
except ImportError:
from ConfigParser import ConfigParser
# ❌ BAD — 2to3 was removed in Python 3.13. This command will fail.
# $ 2to3 -w mypackage/
# ModuleNotFoundError: No module named 'lib2to3'
# ✅ 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/
TypeError when you concatenate str and bytes. In Python 2 this worked silently via implicit ASCII encoding. Fix: explicitly .encode() or .decode() at every I/O boundary. Run tests with python3 -bb. [src1, src3]5 / 2 returns 2 in Python 2 but 2.5 in Python 3. This causes subtle bugs in index calculations and pagination. Fix: add from __future__ import division to all files and use // for intentional floor division. [src1, src5]dict.keys()[0]) fails in Python 3. Fix: wrap in list() where random access is needed, or iterate directly. [src2, src6]b"abc"[0] returns 97 (int) in Python 3, not b"a". Fix: use b"abc"[0:1] for a bytes slice, or chr(b"abc"[0]) for a character. [src1]SyntaxError in Python 3. Fix: add from __future__ import print_function as the first migration step in every file. [src2]reduce moved to functools.reduce, reload to importlib.reload, unicode to str, long to int. Fix: run futurize to catch most renames; check remaining ImportErrors manually. [src2, src4]None < 1 raises TypeError). Fix: add explicit key= functions to sorted() calls and handle None values before comparison. [src5]'rb'); Python 3 csv requires text mode with newline=''. Fix: use open(f, 'r', newline='', encoding='utf-8') for Python 3. [src5]2to3 or lib2to3 will hit ModuleNotFoundError on Python 3.13+. Fix: switch to futurize or LibCST for automated code transformation. [src2, src8]# 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 | Status | Breaking Changes | Migration Notes |
|---|---|---|---|
| Python 3.14 (Oct 2025) | Current | from __future__ import annotations deprecated (PEP 649); incremental GC | Deferred annotation evaluation is now native; template strings added |
| Python 3.13 (Oct 2024) | Stable | 2to3 / lib2to3 removed; PEP 594 removes 20 stdlib modules | Use futurize or LibCST instead; check for removed modules |
| Python 3.12 (Oct 2023) | Stable | Improved error messages; 2to3 still available but deprecated | Last Python version with 2to3 in stdlib |
| Python 3.10 | Security | collections.abc must be imported explicitly | from collections.abc import Mapping |
| Python 3.8-3.9 | EOL | Assignment expressions (:=), positional-only params | Stable target for most migrations |
| Python 3.6-3.7 | EOL | f-strings, dict insertion order guaranteed (3.7) | Minimum recommended target for new migrations |
| Python 2.7 | EOL (Jan 2020) | Last Python 2 release ever | Required as migration source baseline |
| Use When | Don't Use When | Use Instead |
|---|---|---|
| Your codebase runs Python 2.7 | Code is already Python 3 only | No migration needed |
| Python 2 EOL means no security patches | Embedded system with frozen Python 2 runtime | Keep Python 2, isolate from network |
| You need async/await, f-strings, type hints | Throwaway script you'll never run again | Leave as-is |
| Third-party deps have dropped Py2 support | Migration blocked by a dep with no Py3 support | Fork/replace the dependency first |
| Team hiring — Python 3 devs are the norm | Legacy system being decommissioned within 6 months | Run out the clock on Python 2 |
| Performance gains (12% CPU, 30% memory) | Code is <100 lines and trivially rewriteable | Rewrite from scratch in Python 3 |
2to3 tool was deprecated in Python 3.11 and removed entirely in Python 3.13 (October 2024). Use futurize (from python-future) or python-modernize (based on six) instead. For advanced transformations, use LibCST. [src2, src8]from __future__ import unicode_literals import can cause unexpected issues with APIs that expect bytes (e.g., some C extensions, os.path on Python 2 Windows). Test thoroughly before adding globally. [src1, src3]python3 -bb is essential during testing — it converts BytesWarning to errors, catching implicit bytes/str mixing that would otherwise be silent. [src1]collections.OrderedDict. [src6]cgi, crypt, imghdr, or other removed modules, you need modern replacements. [src1]from __future__ import annotations (PEP 563) was deprecated in Python 3.14 in favor of PEP 649's native deferred evaluation. While still functional, plan for its eventual removal (not before 2029). [src1]