The main breaking change is that you can't read a stream of bytes from a byte-oriented resource (say, a network connection that by definition can only send a stream of octets) and magically treat it like text elsewhere. While that's a tempting thing to want to do, it's been an unending fountain of bugs over the years. For example, in Python 2.7:
>>> a = 'this is a test ಠ_ಠ'
>>> b = bytes(a)
>>> b
'this is a test \xe0\xb2\xa0_\xe0\xb2\xa0'
>>> type(b)
<type 'str'>
In Python 3.5: >>> a = 'this is a test ಠ_ಠ'
>>> print(a)
this is a test ಠ_ಠ
>>> b = bytes(a)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: string argument without an encoding
>>> b = bytes(a.encode('utf-8'))
>>> b
b'this is a test \xe0\xb2\xa0_\xe0\xb2\xa0'
>>> type(b)
<class 'bytes'>
In 2.7, you encode a series of Unicode values back to... a string? This is the source of all kinds of fun as functions expecting a string as input don't have a way to tell whether it's already been encoded or if they need to do so. In 3.5, there's no ambiguity. Functions that write to files or network sockets can either receive strings and know that they have to encode the strings locally, or bytes and know that they don't have to.Right up near the top of Zen we get:
>>> import this
The Zen of Python, by Tim Peters
Beautiful is better than ugly.
Explicit is better than implicit.
The Python 2-style implicit conversions were awfully convenient 15 years ago when most things were ASCII. They're absolutely painful now that we've moved past the expectation that strings are arrays of single bytes.