NexusCS

Python 2 vs 3

Python
Side-by-side comparison of Python 2 and Python 3 syntax differences for migration and compatibility.
python
migration
compatibility

Getting started

Introduction

Python 3 introduced breaking changes to improve consistency and remove legacy quirks. This cheatsheet covers the major syntax and behavior differences between Python 2.7 and Python 3.x.

Quick migration

# Check Python version
python --version
python3 --version

# Run 2to3 conversion tool
2to3 script.py
2to3 -w script.py  # Write changes

# Run with Python 3
python3 script.py

Version check in code

# Python 2 & 3 compatible
import sys

if sys.version_info[0] < 3:
    print("Python 2")
else:
    print("Python 3")

# Python 3.6+
if sys.version_info >= (3, 6):
    print("Modern Python 3")

Print statement vs function

Python 2 (statement)

# Print statement
print "Hello"
print "Hello", "World"

# Multiple arguments
print "x =", 42

# Without newline
print "Hello",

# To file
print >> sys.stderr, "Error"

Python 3 (function)

# Print function
print("Hello")
print("Hello", "World")

# Multiple arguments
print("x =", 42)

# Without newline
print("Hello", end="")

# To file
print("Error", file=sys.stderr)

Compatible approach

# Works in both 2 & 3
from __future__ import print_function

print("Hello")
print("x =", 42)
print("Hello", end="")

Integer division

Python 2

# Integer division by default
5 / 2        # 2
5.0 / 2      # 2.5

# Floor division
5 // 2       # 2

# True division (opt-in)
from __future__ import division
5 / 2        # 2.5

Python 3

# True division by default
5 / 2        # 2.5
5.0 / 2      # 2.5

# Floor division
5 // 2       # 2
-5 // 2      # -3

# Always returns float
10 / 5       # 2.0 (not 2)

Migration tip

# Python 2 & 3 compatible
from __future__ import division

# Use // for integer division
count = total // batch_size

# Use / for float division
average = sum / count

String and Unicode

Python 2

# str is bytes
s = "hello"          # <type 'str'>
type(s)              # bytes

# Unicode strings
u = u"hello"         # <type 'unicode'>
u = unicode("hello")

# Mixing causes issues
"hello" + u"world"   # Works
b"hello" + u"world"  # TypeError

Python 3

# str is Unicode
s = "hello"          # <class 'str'>
type(s)              # str (Unicode)

# Bytes strings
b = b"hello"         # <class 'bytes'>
b = bytes("hello", "utf-8")

# No mixing
"hello" + "world"    # Works
b"hello" + "world"   # TypeError

Encoding/decoding

# Python 2
text = u"hello"
data = text.encode("utf-8")  # str
text = data.decode("utf-8")  # unicode

# Python 3
text = "hello"
data = text.encode("utf-8")  # bytes
text = data.decode("utf-8")  # str

Range and xrange

Python 2

# range() returns list
range(5)             # [0, 1, 2, 3, 4]
type(range(5))       # <type 'list'>

# xrange() returns iterator
xrange(5)            # xrange(5)
list(xrange(5))      # [0, 1, 2, 3, 4]

# Memory efficient for loops
for i in xrange(1000000):
    pass

Python 3

# range() returns iterator
range(5)             # range(0, 5)
type(range(5))       # <class 'range'>

# Convert to list
list(range(5))       # [0, 1, 2, 3, 4]

# No xrange()
xrange(5)            # NameError

# Memory efficient by default
for i in range(1000000):
    pass

Compatible code

# Python 2 & 3
try:
    range = xrange
except NameError:
    pass  # Python 3

for i in range(100):
    print(i)

Dictionary methods

Python 2

d = {"a": 1, "b": 2}

# Returns lists
d.keys()             # ['a', 'b']
d.values()           # [1, 2]
d.items()            # [('a', 1), ('b', 2)]

# Separate iterator methods
d.iterkeys()         # <dictionary-keyiterator>
d.itervalues()       # <dictionary-valueiterator>
d.iteritems()        # <dictionary-itemiterator>

Python 3

d = {"a": 1, "b": 2}

# Returns view objects
d.keys()             # dict_keys(['a', 'b'])
d.values()           # dict_values([1, 2])
d.items()            # dict_items([('a', 1), ('b', 2)])

# Convert to list
list(d.keys())       # ['a', 'b']

# No iter* methods
d.iterkeys()         # AttributeError

View objects

# Python 3 views are dynamic
d = {"a": 1}
keys = d.keys()
d["b"] = 2
list(keys)           # ['a', 'b']

# Views support set operations
d1 = {"a": 1, "b": 2}
d2 = {"b": 2, "c": 3}
d1.keys() & d2.keys()  # {'b'}

Input functions

Python 2

# raw_input() returns string
name = raw_input("Name: ")
type(name)           # <type 'str'>

# input() evaluates code
num = input("Number: ")  # User types: 42
type(num)            # <type 'int'>

# DANGEROUS - avoid input()
input("Enter: ")     # User types: __import__('os').system('ls')

Python 3

# input() returns string
name = input("Name: ")
type(name)           # <class 'str'>

# raw_input() doesn't exist
raw_input("Name: ")  # NameError

# Convert manually
num = int(input("Number: "))
type(num)            # <class 'int'>

Compatible approach

# Python 2 & 3
try:
    input = raw_input
except NameError:
    pass  # Python 3

name = input("Name: ")

Exception syntax

Python 2

# Old syntax (comma)
try:
    risky()
except ValueError, e:
    print(e)

# Multiple exceptions
except (TypeError, KeyError), e:
    print(e)

# New syntax also works
except ValueError as e:
    print(e)

Python 3

# Only 'as' syntax
try:
    risky()
except ValueError as e:
    print(e)

# Multiple exceptions
except (TypeError, KeyError) as e:
    print(e)

# Old syntax removed
except ValueError, e:  # SyntaxError
    print(e)

Raising exceptions

# Python 2
raise ValueError, "message"
raise ValueError("message")

# Python 3 (only)
raise ValueError("message")

# With traceback
raise ValueError("message") from original_exception

Import changes

Python 2

# Implicit relative imports
from . import module  # Explicit (recommended)
import module         # May import local module

# Absolute imports (opt-in)
from __future__ import absolute_import
import module         # Always absolute

# Package structure
mypackage/
  __init__.py
  module.py
  subpkg/
    __init__.py

Python 3

# Only explicit relative imports
from . import module  # Relative
from .. import module # Parent package
import module         # Always absolute

# Implicit relative imports removed
import module         # Never searches package

# Package structure (same)
mypackage/
  __init__.py
  module.py
  subpkg/
    __init__.py

Migration

# Python 2 compatible imports
from __future__ import absolute_import

# Explicit relative
from . import sibling
from .sibling import function

# Explicit absolute
import mypackage.module

Iterators

Python 2

# Returns lists
map(str, [1, 2, 3])     # ['1', '2', '3']
filter(None, [0, 1, 2]) # [1, 2]
zip([1, 2], [3, 4])     # [(1, 3), (2, 4)]

# Iterator versions
from itertools import imap, ifilter, izip
imap(str, [1, 2, 3])    # <itertools.imap>

# range also returns list
range(5)                # [0, 1, 2, 3, 4]

Python 3

# Returns iterators
map(str, [1, 2, 3])     # <map object>
filter(None, [0, 1, 2]) # <filter object>
zip([1, 2], [3, 4])     # <zip object>

# Convert to list
list(map(str, [1, 2, 3]))  # ['1', '2', '3']

# No imap, ifilter, izip
from itertools import imap  # ImportError

# range returns iterator
range(5)                # range(0, 5)

Iteration patterns

# Python 3 - more memory efficient
nums = map(lambda x: x * 2, range(1000000))
for n in nums:  # Computed on demand
    pass

# Force evaluation
result = list(map(str, [1, 2, 3]))

String formatting

Old style (%)

# Python 2 & 3
name = "World"
"Hello %s" % name
"x = %d, y = %d" % (1, 2)

# Named arguments
"%(name)s is %(age)d" % {"name": "Alice", "age": 30}

# Format codes
"%10s" % "right"     # Right-aligned
"%-10s" % "left"     # Left-aligned
"%.2f" % 3.14159     # Decimal places

.format() method

# Python 2.6+ & 3
"Hello {}".format("World")
"{} {}".format("Hello", "World")

# Positional
"{0} {1}".format("Hello", "World")
"{1} {0}".format("World", "Hello")

# Named
"{name} is {age}".format(name="Alice", age=30)

# Format specs
"{:>10}".format("right")
"{:.2f}".format(3.14159)

f-strings (3.6+)

# Python 3.6+
name = "World"
f"Hello {name}"

age = 30
f"Age: {age}"

# Expressions
f"2 + 2 = {2 + 2}"

# Format specs
value = 3.14159
f"Value: {value:.2f}"

# Debugging (3.8+)
f"{name=}, {age=}"

Class definitions

Python 2

# Old-style class
class OldStyle:
    pass

# New-style class (recommended)
class NewStyle(object):
    pass

# Differences
old = OldStyle()
new = NewStyle()
type(old)  # <type 'instance'>
type(new)  # <class '__main__.NewStyle'>

Python 3

# All classes are new-style
class MyClass:
    pass

# Explicit object inheritance
class MyClass(object):
    pass

# Both are equivalent
c = MyClass()
type(c)    # <class '__main__.MyClass'>

super() changes

# Python 2
class Child(Parent):
    def method(self):
        super(Child, self).method()

# Python 3
class Child(Parent):
    def method(self):
        super().method()  # Simpler!

Library renames

Standard library

# Python 2 → Python 3
ConfigParser → configparser
Queue → queue
SocketServer → socketserver
repr → reprlib
StringIO → io.StringIO
cStringIO → io.StringIO
cPickle → pickle
thread → _thread
dummy_thread → _dummy_thread
Tkinter → tkinter
tkMessageBox → tkinter.messagebox

urllib changes

# Python 2
import urllib
import urllib2
import urlparse

urllib.urlopen()
urllib2.urlopen()
urlparse.urlparse()

# Python 3
import urllib.request
import urllib.parse
import urllib.error

urllib.request.urlopen()
urllib.parse.urlparse()

six library

# Compatibility library
import six

# Python 2/3 compatible
six.text_type          # unicode/str
six.string_types       # basestring/(str,)
six.iteritems(d)       # d.iteritems()/d.items()
six.moves.urllib       # Unified urllib

Migration tools

2to3 tool

# Analyze changes
2to3 script.py

# Show differences
2to3 -d script.py

# Write changes
2to3 -w script.py

# Backup originals
2to3 -w -n script.py

# Process directory
2to3 -w mypackage/

Common fixes

# 2to3 transformations
print "x"print("x")
except E, e:except E as e:
d.keys()list(d.keys())
xrange()range()
raw_input()input()
unicode()str()
unichr()chr()

future imports

# Python 2 compatibility
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals

# Enable Python 3 behavior in Python 2
print("Hello")      # Function
5 / 2               # 2.5
s = "unicode"       # Unicode by default

Additional differences

Comparison operators

# Python 2
1 < "2"              # True (arbitrary)
None < 0             # True

# Python 3
1 < "2"              # TypeError
None < 0             # TypeError

# Must compare same types

Next method

# Python 2
it = iter([1, 2, 3])
it.next()            # 1

# Python 3
it = iter([1, 2, 3])
next(it)             # 1
it.__next__()        # 2

Metaclasses

# Python 2
class Meta(type):
    pass

class MyClass(object):
    __metaclass__ = Meta

# Python 3
class Meta(type):
    pass

class MyClass(metaclass=Meta):
    pass

Gotchas

Dictionary iteration

# Python 2 - modifying dict while iterating
for key in d.keys():  # Creates list copy
    if condition:
        del d[key]    # Safe

# Python 3 - views are dynamic
for key in d.keys():  # View object
    if condition:
        del d[key]    # RuntimeError

# Fix
for key in list(d.keys()):
    del d[key]

Integer types

# Python 2
type(1)              # <type 'int'>
type(1L)             # <type 'long'>
sys.maxint           # 9223372036854775807

# Python 3
type(1)              # <class 'int'>
type(1L)             # SyntaxError
sys.maxsize          # 9223372036854775807
# No separate long type

File I/O

# Python 2
open("file.txt")     # Text mode, bytes
open("file.txt", "rb")  # Binary mode

# Python 3
open("file.txt")     # Text mode, unicode
open("file.txt", "rb")  # Binary mode, bytes

# Specify encoding
open("file.txt", encoding="utf-8")

Also see

Python 2 vs 3 Cheatsheet - NexusCS