Migrating to Python 3
The concrete changes between the versions, the automated tools, and the semantic diffs a compiler will not catch.
What actually differs
| Task | Python 2 | Python 3 | Note |
|---|---|---|---|
| Output | print x | print(x) | The future import makes the new form work in both |
| Integer division | 5 / 2 is 2 | 5 / 2 is 2.5 | Use // where you meant flooring |
| Iterate a dict | d.iteritems() | d.items() | Removed; items() is a view, not a list |
| Long range | xrange(n) | range(n) | range is lazy in Python 3 |
| Exception handling | except E, e: | except E as e: | A syntax error under Python 3 |
| Raising | raise E, msg | raise E(msg) | The old form does not even parse |
| Text type | unicode | str | bytes is a separate, non-text type |
| Byte literal | "bytes" | b"bytes" | Python 2 lets you omit the prefix |
| Class declaration | class C(object): | class C: | New-style classes are the only kind |
| Sorting | cmp(a, b) | key= functions | functools.cmp_to_key covers leftovers |
| Dict order | Arbitrary hash order | Insertion order since 3.7 | Do not depend on it either way |
| Relative import | Gives package modules | Needs an explicit dot | Absolute imports are the default |
from __future__ import print_function, division, unicode_literals, absolute_import
try:
text_type = unicode # Python 2
except NameError:
text_type = str # Python 3
rows = [{"amount": "3"}, {"amount": "4"}]
print("total:", sum(int(r["amount"]) for r in rows)) # int(3) + int(4)
print(type("label") is text_type) # True on both interpretersTooling and order of work
# 1. Automated fixer. Read every diff it produces; do not merge blind.
2to3 -w -n -f print -f except -f raise mypkg/
# 2. Or, when the code must keep running on 2.7 during the transition:
futurize --stage1 -w mypkg/ # adds __future__ imports only
futurize --stage2 -w mypkg/ # rewrites toward a shared 2/3 subset
# 3. Prove both interpreters still agree, in CI, on every commit.
tox -e py27,py38import six
if isinstance(value, six.string_types): # str/unicode in 2, str in 3
...
for key, row in six.iteritems(table): # items() in 3, iteritems() in 2
...
from six.moves import urllib # renamed module shims
urllib.parse.urlencode(params)
text = six.text_type(blob) # unicode in 2, str in 3- Start with the dependencies: a package with no Python 3 release blocks everything above it, and that is a scheduling problem, not a coding one.
- Add tests before the rewrite. Without a suite the only way to know a port is correct is to read every line, which is slower and less reliable.
- Run the future imports stage first and keep the suite green, then split
/into/and//deliberately. - Grep for the silent hazards:
.iteritems(,has_key(,cmp(,xrange(, barestr(around possibly-unicode values, and every remainingopen(.
⚠️
A file that compiles is not a file that behaves the same. The dangerous changes are semantic — division, text versus bytes, dictionary ordering, and comparison functions — and the automated tools only fix the syntax. Port module by module, keep Python 2 and Python 3 running in CI together, and delete the compatibility shims only after 2.7 is finally gone.
FAQ
Should I use 2to3 or futurize?
Use
2to3 when the target is Python 3 only and the code can stop running on 2.7. Use futurize (or python-modernize) when the same branch must support both during the transition — those tools add __future__ imports and rewrites that keep Python 2 working.What breaks that the tools do not catch?
Integer division, implicit str/unicode coercion, code that relies on dict iteration order, and libraries returning bytes where you assumed text. They compile cleanly and fail at runtime on real data, so they need tests and a careful grep as described above.
Related
unicode and str are different things Python: getting started
Last refreshed 2026-09-18.