How to Migrate from Python 2 to Python 3
How do I migrate from Python 2 to Python 3?
TL;DR
- Bottom line: Use automated tools (
futurizeorpython-modernize) to handle syntax changes, then manually fix unicode/bytes boundaries and test withpython3 -bbto catch type-mixing bugs. [src1, src2] - Key tool/command:
futurize --stage1 --stage2 -w mypackage/ - Watch out for: String/bytes conflation — Python 3 strictly separates
str(text) frombytes(binary), and code that mixes them will raiseTypeErrorat runtime. [src1, src3] - Works with: Python 2.7 (minimum source) to Python 3.8+ (target). Python 2 reached EOL January 2020; 2to3 removed in Python 3.13 (2024).
Constraints
- Python 2.7 required as baseline: All migration tools require Python 2.7 as the minimum source version. Python 2.6 and earlier must first upgrade to 2.7 before any migration can begin. [src1]
- 2to3/lib2to3 removed in Python 3.13: The
2to3tool andlib2to3module were deprecated in Python 3.11 and removed entirely in Python 3.13 (October 2024). Usefuturizefrompython-future,python-modernize, orLibCSTinstead. [src2, src8] - Never mix str and bytes implicitly: Python 3 raises
TypeErrorwhen concatenating or comparingstrandbytes. Every I/O boundary (file, network, database) must explicitly.encode()or.decode(). [src1, src3] from __future__ import annotationsdeprecated 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]- Dictionary insertion order guaranteed only from Python 3.7+: Do not rely on
dictordering if targeting Python 3.6 or earlier. Usecollections.OrderedDictfor guaranteed ordering on older targets. [src6]
Quick Reference
| 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] |
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
- String/bytes TypeError at runtime: Python 3 raises
TypeErrorwhen you concatenatestrandbytes. In Python 2 this worked silently via implicit ASCII encoding. Fix: explicitly.encode()or.decode()at every I/O boundary. Run tests withpython3 -bb. [src1, src3] - Integer division behavior change:
5 / 2returns2in Python 2 but2.5in Python 3. This causes subtle bugs in index calculations and pagination. Fix: addfrom __future__ import divisionto all files and use//for intentional floor division. [src1, src5] - dict.keys()/values()/items() return views, not lists: Code that indexes into these results (
dict.keys()[0]) fails in Python 3. Fix: wrap inlist()where random access is needed, or iterate directly. [src2, src6] - bytes indexing returns int, not single-byte string:
b"abc"[0]returns97(int) in Python 3, notb"a". Fix: useb"abc"[0:1]for a bytes slice, orchr(b"abc"[0])for a character. [src1] - print statement vs function: Forgetting parentheses is a
SyntaxErrorin Python 3. Fix: addfrom __future__ import print_functionas the first migration step in every file. [src2] - Removed modules and renamed builtins:
reducemoved tofunctools.reduce,reloadtoimportlib.reload,unicodetostr,longtoint. Fix: run futurize to catch most renames; check remainingImportErrors manually. [src2, src4] - sorted() and comparison operators: Python 3 removed implicit comparisons between incompatible types (e.g.,
None < 1raisesTypeError). Fix: add explicitkey=functions tosorted()calls and handleNonevalues before comparison. [src5] - csv module encoding change: Python 2 csv requires binary mode (
'rb'); Python 3 csv requires text mode withnewline=''. Fix: useopen(f, 'r', newline='', encoding='utf-8')for Python 3. [src5] - 2to3 removed in Python 3.13: Developers still relying on
2to3orlib2to3will hitModuleNotFoundErroron Python 3.13+. Fix: switch tofuturizeorLibCSTfor automated code transformation. [src2, src8] - PEP 594 stdlib module removals in Python 3.13: Twenty legacy modules were removed (aifc, audioop, cgi, cgitb, chunk, crypt, imghdr, mailcap, etc.). Fix: find modern replacements before targeting Python 3.13+. [src1]
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
| 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 |
When to Use / When Not to Use
| 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 |
Important Caveats
- Python 2 reached end-of-life on January 1, 2020 (PEP 373). No security patches, bug fixes, or updates are available. Running Python 2 in production is a security risk. [src1]
- The
2to3tool was deprecated in Python 3.11 and removed entirely in Python 3.13 (October 2024). Usefuturize(frompython-future) orpython-modernize(based onsix) instead. For advanced transformations, useLibCST. [src2, src8] - The
from __future__ import unicode_literalsimport can cause unexpected issues with APIs that expectbytes(e.g., some C extensions,os.pathon Python 2 Windows). Test thoroughly before adding globally. [src1, src3] - Dropbox's migration of 1M+ lines took approximately 2 years from initial exploration to full rollout, with string encoding being the dominant challenge. Plan accordingly for large codebases. [src3]
- Running
python3 -bbis essential during testing — it convertsBytesWarningto errors, catching implicit bytes/str mixing that would otherwise be silent. [src1] - Dictionary ordering is guaranteed (insertion order) only from Python 3.7+. If targeting 3.6, use
collections.OrderedDict. [src6] - Python 3.13 removed 20 standard library modules under PEP 594 (dead batteries removal). If your Python 2 code imports
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]- Python 3.10 reaches end-of-life in October 2026. Target Python 3.11 or newer for any net-new migration to avoid an immediate second upgrade cycle. [src1]