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
| Topic | What it covers | |
|---|---|---|
| The print statement and integer division | Two Python 2 behaviours that change results without a warning: print as a statement, and division that floors between | lesson |
| unicode and str are different things | In Python 3 there is one text type, str, and one byte type, bytes. In Python 2 the names are reused: str holds bytes | lesson |
| Migrating to Python 3 | The concrete changes between the versions, the automated tools, and the semantic diffs a compiler will not catch | lesson |
| Running Python 2 today: interpreter, pip and virtualenv | Python 2.7 reached end of life in January 2020. It receives no security fixes, no build fixes for new compilers, and no | lesson |
| Lists, dicts and the Python 2-only APIs | The performance reason for iteritems disappeared: in Python 3 items() is already lazy and does not build a list. The | lesson |
| Old-style vs new-style classes and metaclasses | Old-style classes existed for backward compatibility with Python 1.x. In Python 3 every class is new-style, so class | lesson |
| Exceptions and the Python 2 except syntax | The compiler catches the syntax changes. What it does not catch is semantics: an except that was silently swallowing | lesson |
| Files, the codecs module and text vs binary I/O | In Python 2 a file opened in text mode still gives you bytes; the mode only affects newline handling on Windows | lesson |
| Testing legacy Python 2 code before you change it | Legacy code usually has no tests and an unclear specification. A characterisation test records what the code currently | lesson |
| Where Python 2 still appears and what to do about it | The pattern is consistent: Python 2 persists in code that is coupled to something you do not control. That coupling | lesson |
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 explicitFull 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 conversionFull 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 3Full 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 wrongFull 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?
Where do the examples come from?
How do I go deeper than a cheat sheet?
Related cheat sheets
Python 3 NumPy pandas Matplotlib Jupyter Notebook Flask
Last refreshed 2026-09-27.