What are the key differences between Python 2 and Python 3

Umesh S
11 min readFeb 21, 2023

--

Python 2 and Python 3 are two different versions of the Python programming language. Python 2 was introduced in 2000, while Python 3 was released in 2008. Both versions of Python are still in use today, but Python 3 is the recommended version for new projects.

Photo by Hitesh Choudhary on Unsplash

Here are some of the key differences between Python 2 and Python 3:

Print Statement vs. Function:

The “print” statement/function is one of the most fundamental ways to display output in Python, and it is one of the key differences between Python 2 and Python 3. In Python 2, the “print” statement is used to output text to the console, while in Python 3, the “print” function is used.

In Python 2, the syntax for the “print” statement is simply:

print "Hello, World!"

This would output the string “Hello, World!” to the console. However, in Python 3, the “print” statement was replaced with the “print” function, and the syntax is slightly different:

print("Hello, World!")

The difference may seem small, but it can have an impact on the way that code is written and read, especially for developers who are used to writing Python 2 code.

One of the key advantages of using the “print” function is that it allows you to pass multiple arguments, which will be printed with a space separator by default. For example:

x = 1
y = 2
z = 3

print(x, y, z)

This would output the numbers 1, 2, and 3 with spaces between them. In Python 2, you would have to concatenate the values into a single string in order to achieve the same effect.

Another advantage of the “print” function is that it can be used as part of a larger expression. For example:

print("The value of x is", x)

This would output the string “The value of x is” followed by the value of x. In Python 2, you would need to use string concatenation or interpolation to achieve the same effect.

It’s worth noting that while the “print” statement is no longer recommended in Python 3, it is still supported for backwards compatibility. However, it’s a good practice to use the “print” function instead, especially for new projects or code that is being updated from Python 2 to Python 3.

Division:

The behavior of the division operator “/” is another key difference between Python 2 and Python 3. In Python 2, the division operator performs integer division when both operands are integers, which can lead to unexpected results. In Python 3, the division operator always performs true division, returning a float.

For example, consider the following code in Python 2:

>>> 5 / 2
2

In this case, the result of the division is 2, even though the correct answer is 2.5. This is because both operands are integers, so Python 2 performs integer division, rounding down to the nearest integer.

In Python 3, the same code would produce a different result:

>>> 5 / 2
2.5

Here, the result is a floating-point number, because Python 3 always performs true division when the division operator is used.

In addition to the division operator, Python 2 also has a separate operator for true division, “//”, which always returns a float. For example:

>>> 5 // 2
2.0

However, this operator is not always used consistently in Python 2 code, which can lead to subtle bugs and unexpected behavior.

In Python 3, the behavior of the division operator has been changed to always perform true division, which makes the behavior of the division operator more intuitive and predictable. This is especially important for numerical calculations and scientific computing, where floating-point numbers are often used.

It’s worth noting that in Python 3, the behavior of the integer division operator “//” has also been changed slightly. When both operands are integers, “//” now returns a float instead of an integer, in order to be consistent with the behavior of the division operator. However, when one or both operands are floating-point numbers, “//” still performs floor division, returning the largest integer less than or equal to the quotient.

Strings and Unicode:

String handling is another area where there are significant differences between Python 2 and Python 3. The main difference is how Unicode and non-Unicode strings are handled.

In Python 2, there are two types of strings: byte strings (or “str” type) and Unicode strings (or “unicode” type). Byte strings are sequences of bytes, while Unicode strings are sequences of Unicode code points. The default string type in Python 2 is the byte string.

Python 3, on the other hand, handles all strings as Unicode strings, with the “str” type being the default string type. This means that all string literals are Unicode by default, and any byte strings must be explicitly converted to Unicode strings using the “decode()” method.

For example, in Python 2, the following code creates a byte string and a Unicode string:

>>> byte_string = "Hello, World!"
>>> unicode_string = u"Hello, World!"

In Python 3, the same code creates two Unicode strings:

>>> string = "Hello, World!"
>>> unicode_string = "Hello, World!"

In addition to the default string type, there are also several other changes to the string handling in Python 3. One of the most significant is the way that Unicode characters are represented in string literals.

In Python 2, Unicode characters can be represented using escape sequences like “\uXXXX”, where XXXX is the hexadecimal code point of the character. For example:

>>> unicode_string = u"\u03a9"

This creates a Unicode string containing the Greek letter omega.

In Python 3, Unicode characters can be included directly in string literals using the Unicode escape sequence “\N{…}”, where the “…” is the name of the character. For example:

>>> unicode_string = "\N{GREEK CAPITAL LETTER OMEGA}"

This creates the same Unicode string as the previous example.

Another difference is the way that string formatting is handled. In Python 2, string formatting can be done using the “%” operator, but this can be error-prone when dealing with Unicode strings. In Python 3, the recommended way to do string formatting is using the “format()” method.

Overall, the changes to string handling in Python 3 make it easier to work with Unicode text and reduce the potential for encoding errors and other problems that can arise when working with non-Unicode text. However, it can also require some changes to existing code that was written for Python 2.

Handling of Exceptions:

Exception handling is another area where there are some important differences between Python 2 and Python 3. The basic concepts are the same, but there are some differences in the syntax and behavior of exception handling in the two versions of Python.

In Python 2, the syntax for handling exceptions uses the “try/except” block. For example, the following code tries to open a file and handles any “IOError” exceptions that occur:

try:
f = open("file.txt", "r")
# do something with the file
f.close()
except IOError:
print("Error: could not open file")

In Python 3, the syntax for handling exceptions is the same as in Python 2, but there is a new syntax called “except … as” that allows you to assign the exception object to a variable. For example:

try:
f = open("file.txt", "r")
# do something with the file
f.close()
except IOError as e:
print("Error: could not open file")
print(e)

This allows you to access information about the exception that occurred, such as the error message or the error code.

Another difference in exception handling between Python 2 and Python 3 is the handling of exceptions that are not subclasses of “Exception”. In Python 2, you can handle these exceptions using the “except Exception” block, which will catch all exceptions including those that are not subclasses of “Exception”. However, in Python 3, this syntax will not work for non-exception classes.

In addition, there are several new built-in exceptions in Python 3, such as the “TimeoutError” and “ConnectionRefusedError” exceptions. These exceptions are designed to make it easier to handle common networking-related errors.

Finally, in Python 3, there is a new syntax for raising exceptions that allows you to include the exception message directly in the “raise” statement. For example:

raise ValueError("Invalid input")

This makes it easier to create custom exceptions and provide informative error messages to users.

Overall, while the basics of exception handling are the same in both Python 2 and Python 3, there are some important syntax and behavior differences that developers need to be aware of when writing code that needs to work in both versions of Python.

Range Function:

The range() function is used to generate a sequence of numbers in Python. However, there are some important differences in how the range() function works in Python 2 and Python 3.

In Python 2, the range() function returns a list of integers. For example, the following code generates a list of integers from 0 to 9:

>>> range(10)
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

In Python 3, the range() function returns a range object. This is a special type of object that generates the numbers on-the-fly as they are needed. For example:

>>> range(10)
range(0, 10)

If you want to generate a list of integers in Python 3, you need to convert the range object to a list using the list() function:

>>> list(range(10))
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

Another difference between the range() function in Python 2 and Python 3 is that in Python 2, there is a separate function called xrange() that is used to generate a range object. In Python 3, the range() function has been updated to generate a range object instead of a list, so there is no need for a separate xrange() function.

The range() function in both Python 2 and Python 3 also allows you to specify a starting value and a step value, in addition to the ending value. For example:

>>> range(0, 10, 2)
[0, 2, 4, 6, 8]

This code generates a list of even numbers between 0 and 10.

In Python 3, the range() function also supports the concept of "lazy evaluation", which means that the range object generates the numbers on-the-fly as they are needed. This can be more efficient than generating a list of all the numbers up front, especially for large ranges.

Overall, the changes to the range() function in Python 3 are designed to improve performance and memory efficiency by generating range objects instead of lists. However, these changes can also require some changes to existing code that was written for Python 2.

Type Annotations:

Type annotations in Python are a way to provide hints to the interpreter about the data type of a variable or function parameter. In Python 2, type annotations were not available, but in Python 3, the syntax for type annotations was introduced.

In Python 3, you can use the syntax variable_name: data_type to indicate the expected data type of a variable. For example, to indicate that a variable x should be an integer, you can use the following syntax:

x: int = 10

This syntax tells the interpreter that the variable x should be of type int. You can also use type annotations for function parameters and return values. For example, the following function takes two integer arguments and returns their sum:

def add_numbers(a: int, b: int) -> int:
return a + b

The type annotations for the function parameters indicate that they should be integers, and the annotation for the return value indicates that the function should return an integer.

Type annotations can also be used with lists, dictionaries, and other data structures. For example, to indicate that a variable should be a list of integers, you can use the following syntax:

numbers: List[int] = [1, 2, 3, 4]

The List[int] syntax indicates that the variable numbers should be a list of integers.

One advantage of using type annotations is that they can help catch type errors at runtime. If you try to assign a value of the wrong data type to a variable with a type annotation, the interpreter will raise a type error. This can help catch bugs early in the development process.

Type annotations are not mandatory in Python 3, and you can still write Python code without using type annotations. However, using type annotations can make your code more readable and easier to maintain, especially in larger codebases. Type annotations can also be helpful for documenting your code, as they make it clear what data types your code is expecting.

In Python 2, type annotations were not available. However, there are third-party libraries, such as mypy, that provide type checking capabilities for Python 2 code. These libraries use syntax similar to the type annotations syntax in Python 3 to indicate expected data types. While type annotations are not built into Python 2, these libraries can provide similar benefits to those provided by type annotations in Python 3.

Metaclass Changes:

Metaclasses in Python are classes that define the behavior of other classes. They are used to customize the way that classes are created and behave at runtime. In Python 2, metaclasses were defined using a special attribute called __metaclass__, while in Python 3, the syntax for defining metaclasses has changed.

In Python 2, to define a class with a metaclass, you would use the following syntax:

class MyClass(object):
__metaclass__ = MyMeta

The __metaclass__ attribute indicates that MyMeta should be used as the metaclass for MyClass. MyMeta is a class that defines the behavior of MyClass.

In Python 3, the syntax for defining a metaclass has changed. You can define a metaclass by passing a metaclass argument to the class statement. For example:

class MyClass(metaclass=MyMeta):
pass

This syntax indicates that MyMeta should be used as the metaclass for MyClass.

In addition to the change in syntax, there have been some changes to the behavior of metaclasses in Python 3. One significant change is that the default metaclass for new-style classes has changed. In Python 2, the default metaclass was type, while in Python 3, the default metaclass is type.__new__.

Another change is that the behavior of the __prepare__ method has changed. In Python 2, this method was not used by default, and had to be defined explicitly in the metaclass. In Python 3, the __prepare__ method is used by default, and is called before the __new__ method to create the namespace for the class.

There have also been changes to the way that metaclasses are inherited in Python 3. In Python 2, the __metaclass__ attribute was inherited from the base class, while in Python 3, the metaclass is determined by a method resolution order that takes into account the metaclass argument of the most derived class.

Overall, the changes to metaclass syntax and behavior in Python 3 are intended to make metaclasses more consistent and predictable. While there may be some changes required to code that uses metaclasses when migrating from Python 2 to Python 3, the new syntax and behavior should be easier to understand and use in the long run.

There are many other differences between Python 2 and Python 3, but these are some of the most notable. While Python 2 is still widely used, it is no longer receiving new feature updates or bug fixes, and it is recommended that new projects use Python 3. However, migrating existing Python 2 code to Python 3 can be challenging, as there are many differences between the two versions.

--

--

Umesh S

Experienced Software Engineer committed to helping others grow and succeed.