Skip to content

What is New in Python 3

Key Python 3 Features and Differences from Python 2

Section titled “Key Python 3 Features and Differences from Python 2”

Python 3 introduced several significant changes and improvements compared to Python 2 (which reached end-of-life in 2020). Understanding these differences helps clarify why certain features in Python 3 work the way they do and provides historical context.

One of the most visible changes. In Python 3, print is a built-in function and requires parentheses () around its arguments. In Python 2, print was a statement and parentheses were optional.

# Python 3 (Correct)
print("Hello, World!")
# Python 2 style (SyntaxError in Python 3)
# print "Hello, World"

The print() function also has optional keyword arguments like sep (separator between arguments) and end (character(s) appended at the end, defaults to newline \n).

# Python 3: Control separator and end character
print("apple", "banana", "cherry", sep=", ")
print("This line stays on the same", end=" ")
print("line.")
# Output:
# apple, banana, cherry
# This line stays on the same line.

In Python 2, suppressing the newline often involved a trailing comma, which is not valid syntax in Python 3 for this purpose.

In Python 3, the division operator / always performs floating-point division, even for integers. This is often more intuitive.

# Python 3
result = 3 / 2
print(result) # Output: 1.5

If you need integer division (discarding the remainder), use the floor division operator //.

# Python 3: Floor division
result_floor = 3 // 2
print(result_floor) # Output: 1
result_neg = -3 // 2
print(result_neg) # Output: -2 (rounds down towards negative infinity)

In Python 2, / performed integer division if both operands were integers (3 / 2 resulted in 1), and floating-point division only if at least one operand was a float (3.0 / 2 resulted in 1.5). This difference was a common source of bugs.

Python 3 uses Unicode (specifically UTF-8 by default in most environments) for all strings (str type). This provides much better support for international characters and emojis directly.

# Python 3
my_string = "你好世界 €"
print(my_string)
print(type(my_string)) # Output: <class 'str'>

For representing sequences of raw bytes, Python 3 uses the bytes type. You can create bytes literals using a b prefix.

# Python 3
my_bytes = b"\x48\x65\x6c\x6c\x6f" # Represents ASCII "Hello"
print(my_bytes)
print(type(my_bytes)) # Output: <class 'bytes'>
# Encoding a string to bytes
encoded_string = my_string.encode('utf-8')
print(encoded_string)
print(type(encoded_string)) # Output: <class 'bytes'>
# Decoding bytes to a string
decoded_string = encoded_string.decode('utf-8')
print(decoded_string) # Output: 你好世界 €

In Python 2, the default str type handled ASCII, and a separate unicode type (often created with a u prefix like u"你好") was needed for Unicode text. Handling the distinction between bytes and text was more complex and error-prone in Python 2.

In Python 3, the range() function returns an immutable sequence type (a ‘range object’) that generates numbers on demand as you iterate over it. This is memory-efficient for large ranges.

# Python 3
my_range = range(1, 10, 2) # Represents 1, 3, 5, 7, 9
print(my_range) # Output: range(1, 10, 2)
print(type(my_range)) # Output: <class 'range'>
for i in my_range:
print(i, end=" ") # Output: 1 3 5 7 9
# Range objects support slicing (Python 3.2+)
print("\n", my_range[1:3]) # Output: range(3, 7, 2)

In Python 2, range() created a full list of numbers in memory, which could be inefficient. Python 2 had a separate xrange() function that behaved like Python 3’s range() (generating numbers lazily). In Python 3, xrange() was removed, and range() adopted its efficient behavior.

Python 3 requires using the as keyword to assign the caught exception object to a variable in an except block.

# Python 3 (Correct)
try:
result = 10 / 0
except ZeroDivisionError as e:
print(f"Caught an exception: {e}")
print(f"Exception type: {type(e)}")
# Python 2 style (SyntaxError in Python 3)
# except ZeroDivisionError, e:

Furthermore, raising exceptions requires enclosing the exception arguments in parentheses.

# Python 3 (Correct)
raise ValueError("Invalid input provided")
# Python 2 style (SyntaxError in Python 3)
# raise ValueError, "Invalid input provided"

In Python 3, the input() function always reads user input as a string. If you need to treat the input as a number, you must explicitly convert it.

# Python 3
user_name = input("Enter your name: ")
print(f"Hello, {user_name}! Type: {type(user_name)}")
user_age_str = input("Enter your age: ")
try:
user_age_int = int(user_age_str)
print(f"Your age is {user_age_int}. Type: {type(user_age_int)}")
except ValueError:
print("Invalid age entered.")

In Python 2, there were two functions: raw_input() (which behaved like Python 3’s input(), always returning a string) and input() (which tried to evaluate the input as Python code, posing a security risk and often causing confusion). Python 3 removed raw_input() and made input() behave safely.

To get the next item from an iterator (like those created by iter() or generator functions), you use the built-in next() function in Python 3.

# Python 3
my_list = [10, 20, 30]
my_iterator = iter(my_list)
item1 = next(my_iterator)
print(item1) # Output: 10
item2 = next(my_iterator)
print(item2) # Output: 20
# Calling the .next() method directly raises an error
# item3 = my_iterator.next() # AttributeError in Python 3

In Python 2, iterators had a .next() method which was called directly. Python 3 standardized on using the next() function.

  • Dictionary Views: Methods like .keys(), .values(), and .items() return memory-efficient ‘view’ objects in Python 3, not lists as in Python 2. You can convert them to lists if needed: list(my_dict.keys()).
  • Metaclass Syntax: The way metaclasses are specified changed.
  • Standard Library Reorganization: Some modules were renamed or moved (e.g., urllib2 became part of urllib).
  • Function Annotations: Python 3 introduced syntax for type hints (typing module).

The __future__ Module (Historical Context)

Section titled “The __future__ Module (Historical Context)”

The __future__ module was used in Python 2 to selectively enable features from Python 3, easing the transition. For example, from __future__ import division would make / perform float division in Python 2.

While historically important for migration, these imports have no effect in Python 3 and are generally not needed in modern Python 3 code.

Python included a command-line tool called 2to3 that could automatically convert most Python 2 code to Python 3 syntax. It applied a series of fixers to handle changes like the print statement becoming a function, renaming modules, and updating exception syntax.

While useful during the major migration period, it’s less relevant now as most active development occurs directly in Python 3. However, understanding its existence provides context for the transition challenges faced years ago.

# Example of a Python 2 code snippet:
def greet(name):
print "Hello,", name
# Running '2to3' on this snippet would suggest changing it to:
def greet(name):
print("Hello,", name)