What are the major differences between Python 2 and Python 3 that still matter today?

6 minintermediatepython2python3unicodehistory

Quick Answer

The most consequential change: **strings are Unicode by default in Python 3** (`str` is text, `bytes` is binary — no more implicit, error-prone mixing of the two as in Python 2's `str`/`unicode` split). Other lasting changes: `print` became a function, `/` performs true division by default (`//` for floor division), and iteration-heavy built-ins (`range`, `dict.keys()`, `map`, `filter`) return lazy iterators/views instead of lists. Python 2 reached end-of-life in January 2020.

Detailed Answer

The biggest change: text vs binary data

# Python 2 (historical) -- 'str' was actually a byte string; 'unicode' was text
s = "hello"        # bytes, in Python 2
u = u"hello"        # explicit unicode, in Python 2
s + u                 # implicit conversion -- worked until it silently didn't, on non-ASCII data

# Python 3 -- unambiguous
s = "hello"    # str -- ALWAYS Unicode text
b = b"hello"    # bytes -- ALWAYS binary data
s + b            # TypeError: can't concat str to bytes -- caught immediately, not silently wrong

Python 2's implicit str/unicode mixing was a constant, subtle source of UnicodeDecodeError crashes in production whenever non-ASCII data appeared somewhere unexpected. Python 3 makes the distinction explicit and enforced at the type level — you must deliberately .encode()/ .decode() to cross between text and bytes, which surfaces the issue immediately during development instead of as an intermittent production bug.

print as a function, not a statement

# Python 2
print "hello"

# Python 3
print("hello")

Making print a real function (rather than special statement syntax) allows it to accept keyword arguments (sep, end, file, flush) and be passed around/reassigned like any other function — a small but representative example of Python 3's broader push toward consistency.

True division by default

# Python 2
5 / 2    # 2  -- integer division by default (surprising for many)
5 / 2.0   # 2.5

# Python 3
5 / 2    # 2.5  -- true division by default
5 // 2    # 2    -- floor division, now an explicit, separate operator

Python 2's / silently performed integer division when both operands were ints — a frequent source of subtle bugs when a variable that was expected to be a float turned out to be an int. Python 3 splits this into two unambiguous operators.

Lazy iterators instead of eager lists

# Python 2
range(10)         # a full list: [0, 1, 2, ..., 9]  -- built immediately in memory
dict.keys()        # a list

# Python 3
range(10)          # a range object -- lazy, O(1) memory regardless of size
dict.keys()         # a view object -- reflects live changes to the dict, no list copy
map(f, items)        # a lazy iterator, not an eagerly built list

This shift (also affecting filter, zip) reflects Python 3's general preference for laziness by default, improving memory efficiency for large ranges/collections — code relying on range(...) behaving like an indexable, sliceable list still mostly works (since range supports indexing/slicing), but code relying on it being an actual list (e.g., isinstance(x, list)) breaks.

Why this still matters today

Python 2 reached official end-of-life on January 1, 2020 — no more security patches, and most major libraries (NumPy, Django, and virtually the entire PyPI ecosystem) dropped Python 2 support around the same time. It matters in interviews mainly as historical/foundational knowledge: understanding why Python 3 made these changes (especially the Unicode/bytes split) demonstrates a solid grasp of Python's string and type model that's directly relevant even though nobody should be writing new Python 2 code today.

Interview-ready summary: The Unicode/bytes split (str is always text, bytes is always binary, no implicit mixing) is the change with the most lasting practical impact, eliminating a whole class of Python 2 encoding bugs. print() as a function, true division by default, and lazy iterators instead of eagerly built lists rounded out Python 3's broader push toward consistency and correctness — Python 2 itself is long past end-of-life (January 2020) and shouldn't appear in new code.

Related Resources