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

TaskPython 2Python 3Note
Outputprint xprint(x)The future import makes the new form work in both
Integer division5 / 2 is 25 / 2 is 2.5Use // where you meant flooring
Iterate a dictd.iteritems()d.items()Removed; items() is a view, not a list
Long rangexrange(n)range(n)range is lazy in Python 3
Exception handlingexcept E, e:except E as e:A syntax error under Python 3
Raisingraise E, msgraise E(msg)The old form does not even parse
Text typeunicodestrbytes is a separate, non-text type
Byte literal"bytes"b"bytes"Python 2 lets you omit the prefix
Class declarationclass C(object):class C:New-style classes are the only kind
Sortingcmp(a, b)key= functionsfunctools.cmp_to_key covers leftovers
Dict orderArbitrary hash orderInsertion order since 3.7Do not depend on it either way
Relative importGives package modulesNeeds an explicit dotAbsolute 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 interpreters

Tooling 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,py38
import 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(, bare str( around possibly-unicode values, and every remaining open(.
⚠️
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.

unicode and str are different things Python: getting started

Last refreshed 2026-09-18.