Python 2.x cheat sheet

A scannable Python 2.x reference: 24 short snippets across 10 topics, each linking back to the lesson it came from.

At a glance

TopicWhat it covers
The print statement and integer divisionTwo Python 2 behaviours that change results without a warning: print as a statement, and division that floors betweenlesson
unicode and str are different thingsIn Python 3 there is one text type, str, and one byte type, bytes. In Python 2 the names are reused: str holds byteslesson
Migrating to Python 3The concrete changes between the versions, the automated tools, and the semantic diffs a compiler will not catchlesson
Running Python 2 today: interpreter, pip and virtualenvPython 2.7 reached end of life in January 2020. It receives no security fixes, no build fixes for new compilers, and nolesson
Lists, dicts and the Python 2-only APIsThe performance reason for iteritems disappeared: in Python 3 items() is already lazy and does not build a list. Thelesson
Old-style vs new-style classes and metaclassesOld-style classes existed for backward compatibility with Python 1.x. In Python 3 every class is new-style, so classlesson
Exceptions and the Python 2 except syntaxThe compiler catches the syntax changes. What it does not catch is semantics: an except that was silently swallowinglesson
Files, the codecs module and text vs binary I/OIn Python 2 a file opened in text mode still gives you bytes; the mode only affects newline handling on Windowslesson
Testing legacy Python 2 code before you change itLegacy code usually has no tests and an unclear specification. A characterisation test records what the code currentlylesson
Where Python 2 still appears and what to do about itThe pattern is consistent: Python 2 persists in code that is coupled to something you do not control. That couplinglesson

Quick snippets

The print statement and integer division

print is a statement, not a function

import sys

print "hello"                  # no parentheses required
print "value:", 42             # a trailing comma means "no newline"
print "still the same line",
print                          # a bare print emits just the newline

print >>sys.stderr, "warning: writing to a file object"
print >>open("out.txt", "w"), "redirected"

print "count = " + 3           # TypeError: cannot concatenate 'str' and 'int'
print "count =", 3             # this works, because print applies str() to each item

Division between integers floors

print 7 / 2        # 3     int / int floors, it does not truncate
print 7.0 / 2      # 3.5   one float operand promotes the result
print -7 / 2       # -4    floors toward negative infinity, so NOT -3
print 7 % -2       # -1    the result takes the sign of the divisor
print 7 // 2       # 3     the explicit floor operator exists in Python 2 too

from __future__ import division    # must be the first statement in the file

print 7 / 2        # 3.5   true division, matching Python 3
print 7 // 2       # 3     integer division is now explicit

Full lesson: The print statement and integer division →

unicode and str are different things

Two string types

# -*- coding: utf-8 -*-
name = "caf\xc3\xa9"       # str: these are raw UTF-8 bytes, not text
text = u"caf\xe9"          # unicode: a sequence of code points

print type(name), len(name)      # <type 'str'> 5   (c, a, f, then two UTF-8 bytes)
print type(text), len(text)      # <type 'unicode'> 4

print name.decode("utf-8")       # u'caf\xe9'  - bytes in, text out
print text.encode("utf-8")       # 'caf\xc3\xa9' - text in, bytes out

Two string types

print text.encode("ascii")    # UnicodeEncodeError: 'ascii' codec can't encode ...
print name + text             # UnicodeDecodeError: the str is decoded as ASCII first
print str(text)               # usually raises, and is never the right conversion

Full lesson: unicode and str are different things →

Migrating to Python 3

What actually differs

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

Tooling and order of work

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

Full lesson: Migrating to Python 3 →

Running Python 2 today: interpreter, pip and virtualenv

Getting a 2.7 interpreter

python2 --version          # 2.7.18 is the final release
python2.7 -c "import sys; print(sys.version_info)"

# containers are the cleanest way to get 2.7 today
docker run --rm -it -v "$PWD":/app -w /app python:2.7.18-slim python --version

# the last pip that supports 2.7
python2 -m pip --version

Isolating a legacy project

python2 -m virtualenv venv2        # note: virtualenv, not the venv module
source venv2/bin/activate

pip install --upgrade "pip<21" "setuptools<45" "wheel<0.38"
pip install -r requirements-legacy.txt

python -c "import sys; print(sys.prefix)"

Installation problems you will hit

# a resolution failure usually means "no 2.7-compatible release exists"
pip install requests
# ERROR: Could not find a version that satisfies the requirement requests

# pin the last release that supported 2.7
pip install "requests==2.27.1"

# build a wheel on a newer machine only if the ABI matches
pip wheel --no-deps -w wheels/ "cffi==1.15.1"

Full lesson: Running Python 2 today: interpreter, pip and virtualenv →

Lists, dicts and the Python 2-only APIs

Dictionary methods

# Python 2
d = {"a": 1, "b": 2}
if d.has_key("a"):
    for k, v in d.iteritems():
        print k, v
keys = d.keys()          # a real list you can sort and mutate

Dictionary methods

# Python 3 equivalent
d = {"a": 1, "b": 2}
if "a" in d:
    for k, v in d.items():
        print(k, v)
keys = list(d.keys())    # copy if you intend to modify during iteration

Builtins and sorting

# Python 2: the cmp= sort argument
def by_length(a, b):
    return cmp(len(a), len(b))

names = ["charlie", "bob", "alice"]
names.sort(cmp=by_length)

# Python 3: key functions, or functools.cmp_to_key when you really need a comparator
names = sorted(["charlie", "bob", "alice"], key=len)
from functools import cmp_to_key
names = sorted(["charlie", "bob", "alice"], key=cmp_to_key(by_length))

Full lesson: Lists, dicts and the Python 2-only APIs →

Old-style vs new-style classes and metaclasses

The two object models

# Python 2 only
class Old:
    pass

class New(object):
    pass

print type(Old)          # <type 'classobj'>
print type(New)          # <type 'type'>
print Old.__mro__        # AttributeError: no MRO
print New.__mro__        # (<class 'New'>, <class 'object'>)

Metaclasses

# Python 2: a module-level attribute on the class body
class Register(object):
    __metaclass__ = RegistryMeta
    name = "plugin"

# Python 3: a keyword argument in the class statement
class Register(metaclass=RegistryMeta):
    name = "plugin"

Full lesson: Old-style vs new-style classes and metaclasses →

Exceptions and the Python 2 except syntax

The comma form

# Python 2
try:
    value = int(raw_input("number: "))
except ValueError, exc:              # binds the exception to exc
    print "bad number:", exc
except (IOError, OSError), exc:
    print "io problem:", exc
except Exception, exc:
    raise RuntimeError("wrapped: %s" % exc)

The comma form

# Python 3 - identical semantics, different syntax
try:
    value = int(input("number: "))
except ValueError as exc:
    print("bad number:", exc)
except (IOError, OSError) as exc:
    print("io problem:", exc)
except Exception as exc:
    raise RuntimeError("wrapped: %s" % exc) from exc

A mechanical rewrite

# 2to3 handles the syntax; review every change it makes
2to3 -w -n app/ | tee 2to3.log

# then look for the patterns it deliberately leaves alone
grep -rn "except:" app/ | grep -v "except Exception"

Full lesson: Exceptions and the Python 2 except syntax →

Files, the codecs module and text vs binary I/O

open() is binary

# Python 2: open() reads bytes. 'r' does newline translation, not decoding.
f = open("notes.txt", "rb")
raw = f.read()                 # str (bytes)
f.close()

print type(raw)                # <type 'str'>
text = raw.decode("utf-8")     # unicode
print type(text)               # <type 'unicode'>

The codecs module

# 'latin-1' is the escape hatch when you must not lose bytes
data = open("unknown.bin", "rb").read()
guessed = data.decode("utf-8", "replace")     # lossy but readable
safe = data.decode("latin-1")                 # lossless, may look wrong

Full lesson: Files, the codecs module and text vs binary I/O →

Testing legacy Python 2 code before you change it

A reproducible 2.7 test environment

python2 -m virtualenv venv2
source venv2/bin/activate
pip install --upgrade "pip<21" "setuptools<45"
pip install -r requirements-legacy.txt
pip install "pytest<5" "mock<4" "tox<4"

pytest -q tests/ --junitxml=before.xml

A reproducible 2.7 test environment

# tox.ini covering both interpreters while you migrate
[tox]
envlist = py27,py311

[testenv]
deps = -rrequirements.txt
commands = pytest -q tests/

Full lesson: Testing legacy Python 2 code before you change it →

Where Python 2 still appears and what to do about it

Convert or contain

Convert when:
  - the code is still evolving and will receive features
  - it handles untrusted input or network traffic
  - it depends on libraries still available under Python 3
  - the module has tests, or is small enough to characterise quickly

Contain when:
  - a third-party binary requires the 2.7 interpreter
  - the code is frozen and will never change
  - migration cost exceeds the cost of running the risk, and the risk is bounded
  - you can isolate it behind a network boundary

A migration plan that survives contact

# 1. inventory what you actually have
grep -rn "print " --include="*.py" . | wc -l
grep -rn "except .*," --include="*.py" .
grep -rn "__metaclass__\|iteritems\|has_key\|xrange\|raw_input\|urllib2" --include="*.py" .

# 2. measure the surface before promising a date
2to3 -f all -n -W app/ | grep -c "^---"

Full lesson: Where Python 2 still appears and what to do about it →

FAQ

Is this Python 2.x cheat sheet free to use?
Yes. No sign-up and no tracking: the page is static, every example is on the page itself, and you can print it or save it as a one-page reference.
Where do the examples come from?
Every snippet is taken from the 10 lessons of the Python 2.x course on this site, and each section links back to the lesson it was pulled from.
How do I go deeper than a cheat sheet?
Open the full Python 2.x course — it carries the worked explanations, the edge cases and the exercises behind every line here.

Python 3 NumPy pandas Matplotlib Jupyter Notebook Flask

Last refreshed 2026-09-27.