Sebastian Raschka
last updated: 05/24/2014

This is a subsection of "A collection of not-so-obvious Python stuff you should know!"

## Key differences between Python 2 and 3

There are some good articles already that are summarizing the differences between Python 2 and 3, e.g.,
- https://wiki.python.org/moin/Python2orPython3 - https://docs.python.org/3.0/whatsnew/3.0.html - http://python3porting.com/differences.html - https://docs.python.org/3/howto/pyporting.html
etc.

But it might be still worthwhile, especially for Python newcomers, to take a look at some of those! (Note: the the code was executed in Python 3.4.0 and Python 2.7.5 and copied from interactive shell sessions.)

### Overview - Key differences between Python 2 and 3

- Unicode
- The print statement
- Integer division
- xrange()
- Raising exceptions
- Handling exceptions
- next() function and .next() method
- Loop variables and leaking into the global scope
- Comparing unorderable types

### Unicode...

[back to Python 2.x vs 3.x overview]

#### - Python 2:

We have ASCII `str()` types, separate `unicode()`, but no `byte` type

#### - Python 3:

Now, we finally have Unicode (utf-8) `str`ings, and 2 byte classes: `byte` and `bytearray`s

    #############
    # Python 2
    #############

    >>> type(unicode('is like a python3 str()'))
    <type 'unicode'>

    >>> type(b'byte type does not exist')
    <type 'str'>

    >>> 'they are really' + b' the same'
    'they are really the same'

    >>> type(bytearray(b'bytearray oddly does exist though'))
    <type 'bytearray'>

    #############
    # Python 3
    #############

    >>> print('strings are now utf-8 \u03BCnico\u0394é!')
    strings are now utf-8 μnicoΔé!

    >>> type(b' and we have byte types for storing data')
    <class 'bytes'>

    >>> type(bytearray(b'but also bytearrays for those who prefer them over strings'))
    <class 'bytearray'>

    >>> 'string' + b'bytes for data'
    Traceback (most recent call last):s
      File "<stdin>", line 1, in <module>
    TypeError: Can't convert 'bytes' object to str implicitly

### The print statement

[back to Python 2.x vs 3.x overview]

Very trivial, but this change makes sense, Python 3 now only accepts `print`s with proper parentheses - just like the other function calls ...

    # Python 2
    >>> print 'Hello, World!'
    Hello, World!
    >>> print('Hello, World!')
    Hello, World!

    # Python 3
    >>> print('Hello, World!')
    Hello, World!
    >>> print 'Hello, World!'
      File "<stdin>", line 1
        print 'Hello, World!'
                            ^
    SyntaxError: invalid syntax

And if we want to print the output of 2 consecutive print functions on the same line, you would use a comma in Python 2, and a `end=""` in Python 3:

    # Python 2
    >>> print "line 1", ; print 'same line'
    line 1 same line

    # Python 3
    >>> print("line 1", end="") ; print (" same line")
    line 1 same line

### Integer division

[back to Python 2.x vs 3.x overview]

This is a pretty dangerous thing if you are porting code, or executing Python 3 code in Python 2 since the change in integer-division behavior can often go unnoticed.
So, I still tend to use a `float(3)/2` or `3/2.0` instead of a `3/2` in my Python 3 scripts to save the Python 2 guys some trouble ... (PS: and vice versa, you can `from __future__ import division` in your Python 2 scripts).

    # Python 2
    >>> 3 / 2
    1
    >>> 3 // 2
    1
    >>> 3 / 2.0
    1.5
    >>> 3 // 2.0
    1.0

    # Python 3
    >>> 3 / 2
    1.5
    >>> 3 // 2
    1
    >>> 3 / 2.0
    1.5
    >>> 3 // 2.0
    1.0

### `xrange()`

[back to Python 2.x vs 3.x overview]

`xrange()` was pretty popular in Python 2.x if you wanted to create an iterable object. The behavior was quite similar to a generator ('lazy evaluation'), but you could iterate over it infinitely. The advantage was that it was generally faster than `range()` (e.g., in a for-loop) - not if you had to iterate over the list multiple times, since the generation happens every time from scratch!
In Python 3, the `range()` was implemented like the `xrange()` function so that a dedicated `xrange()` function does not exist anymore.

    # Python 2
    > python -m timeit 'for i in range(1000000):' ' pass'
    10 loops, best of 3: 66 msec per loop

        > python -m timeit 'for i in xrange(1000000):' ' pass'
    10 loops, best of 3: 27.8 msec per loop

    # Python 3
    > python3 -m timeit 'for i in range(1000000):' ' pass'
    10 loops, best of 3: 51.1 msec per loop

    > python3 -m timeit 'for i in xrange(1000000):' ' pass'
    Traceback (most recent call last):
      File "/Library/Frameworks/Python.framework/Versions/3.4/lib/python3.4/timeit.py", line 292, in main
        x = t.timeit(number)
      File "/Library/Frameworks/Python.framework/Versions/3.4/lib/python3.4/timeit.py", line 178, in timeit
        timing = self.inner(it, self.timer)
      File "<timeit-src>", line 6, in inner
        for i in xrange(1000000):
    NameError: name 'xrange' is not defined

### Raising exceptions

[back to Python 2.x vs 3.x overview]

Where Python 2 accepts both notations, the 'old' and the 'new' way, Python 3 chokes (and raises a `SyntaxError` in turn) if we don't enclose the exception argument in parentheses:

    # Python 2
    >>> raise IOError, "file error"
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    IOError: file error
    >>> raise IOError("file error")
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    IOError: file error

    # Python 3
    >>> raise IOError, "file error"
      File "<stdin>", line 1
        raise IOError, "file error"
                     ^
    SyntaxError: invalid syntax
    >>> raise IOError("file error")
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    OSError: file error

### Handling exceptions

[back to Python 2.x vs 3.x overview]

Also the handling of exceptions has slightly changed in Python 3. Now, we have to use the `as` keyword!

    # Python 2
    >>> try:
    ...     blabla
    ... except NameError, err:
    ...     print err, '--> our error msg'
    ...
    name 'blabla' is not defined --> our error msg

    # Python 3
    >>> try:
    ...     blabla
    ... except NameError as err:
    ...     print(err, '--> our error msg')
    ...
    name 'blabla' is not defined --> our error msg

### The `next()` function and `.next()` method

[back to Python 2.x vs 3.x overview]

Where you can use both function and method in Python 2.7.5, the `next()` function is all that remain in Python 3!

    # Python 2
    >>> my_generator = (letter for letter in 'abcdefg')
    >>> my_generator.next()
    'a'
    >>> next(my_generator)
    'b'

    # Python 3
    >>> my_generator = (letter for letter in 'abcdefg')
    >>> next(my_generator)
    'a'
    >>> my_generator.next()
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    AttributeError: 'generator' object has no attribute 'next'

### In Python 3.x for-loop variables don't leak into the global namespace anymore

[back to Python 2.x vs 3.x overview]

This goes back to a change that was made in Python 3.x and is described in What’s New In Python 3.0 as follows:

"List comprehensions no longer support the syntactic form `[... for var in item1, item2, ...]`. Use `[... for var in (item1, item2, ...)]` instead. Also note that list comprehensions have different semantics: they are closer to syntactic sugar for a generator expression inside a `list()` constructor, and in particular the loop control variables are no longer leaked into the surrounding scope."

`[In:]`

    from platform import python_version
    print('This code cell was executed in Python', python_version())

    i = 1
    print([i for i in range(5)])
    print(i, '-> i in global')

`[Out:]`

    This code cell was executed in Python 3.3.5
    [0, 1, 2, 3, 4]
    1 -> i in global

`[In:]`

    from platform import python_version
    print 'This code cell was executed in Python', python_version()

    i = 1
    print [i for i in range(5)]
    print i, '-> i in global'

`[Out:]`

    This code cell was executed in Python 2.7.6
    [0, 1, 2, 3, 4]
    4 -> i in global

#### Python 3.x prevents us from comparing unorderable types

[back to Python 2.x vs 3.x overview]

`[In:]`

    from platform import python_version
    print 'This code cell was executed in Python', python_version()

    print [1, 2] > 'foo'
    print (1, 2) > 'foo'
    print [1, 2] > (1, 2)

`[Out:]`

    This code cell was executed in Python 2.7.6
    False
    True
    False

`[In:]`

    from platform import python_version
    print('This code cell was executed in Python', python_version())

    print([1, 2] > 'foo')
    print((1, 2) > 'foo')
    print([1, 2] > (1, 2))

`[Out:]`

    This code cell was executed in Python 3.3.5
    ---------------------------------------------------------------------------
    TypeError                                 Traceback (most recent call last)
    <ipython-input-3-1d774c677f73> in <module>()
          2 print('This code cell was executed in Python', python_version())
          3
    ----> 4 [1, 2] > 'foo'
          5 (1, 2) > 'foo'
          6 [1, 2] > (1, 2)

    TypeError: unorderable types: list() > str()