The entire software engineering community has focused on converting their Python 2.7 code to Python 3. From Jan 1st 2020, Python 2.7 will no longer be officially supported. But beyond gaining ability to use a few new cool modules and having future support, what are you actually gaining by moving to Python 3? In this post I will explore some of the changes that have made me an impression and also motivated me to move my code to Python 3 with simple examples. If you are looking for the entire list from the Python Software Foundation, you can find it here.

Dictionaries Preserve Order

For me, this is the biggest one. You had to use OrderedDict before, now it’s the default data structure. No more unordered lists of dictionary keys. PEP-372

abc = {'one': 1, 'two': 2, 'three': 3}

print([k for k in abc])

# ['two', 'three', 'one']

abc = {'one': 1, 'two': 2, 'three': 3}

print([k for k in abc])

# ['one', 'two', 'three']

Dictionary Unpacking

Beyond the obvious shorthand syntax, it only merges the top level of keys and values. It doesn’t traverse nested dictionaries. PEP-448

def merge_dicts(a, b):
    c = a.copy()
    c.update(b)
    return c

c = merge_dicts(a, b)
c = {**a, **b}

Iterables Everywhere

Functions like map, range, dict.values() all return iterables, thus making them more memory efficient. This means you need to explicitly cast to the type you want. It makes the code more readable and self-documenting. PEP-3106

a = {
    'a': 1,
    'b': 2
}

my_vals = a.values()
# [1, 2]
a = {
    'a': 1,
    'b': 2
}

my_vals = a.values()
# dict_values([1, 2])

my_set = set(a.values())
# {1,2}

my_list = list(a.values())
# [1,2]

Yield

The syntax for yielding from a generator has never been cleaner. As Python 3 is iterable-oriented, majority of the syntax around common operations has received a facelift. PEP-380

for iter in generator():
    yield iter
yield from generator()

f-Strings

As a person who has spent a significant amount doing ES6 and TypeScript, it’s easy to get used to the backtick syntax and embedding the variables inplace. PEP-498

name = "Ivo"
msg = "Hello {}".format(name)
name = "Ivo"
msg = f"Hello {name}"

Unicode Strings by Default

The changes in the area will definitely bring in some confusion. The Python 2 implicit str type is ASCII, in Python 3 is unicode. PEP-3120

'ivo'
# str

b'ivo'
# str

u'ivo'
# unicode
'ivo'
# str

b'ivo'
# bytes

u'ivo'
# str

Matrix Multiplication

A new shorthand for multiplication of matrices. PEP-465

import numpy as np

a = np.array([[1, -1], [-1, 1]])
b = np.array([[-1, 1], [1, -1]])

np.dot(a, b)
import numpy as np

a = np.array([[1, -1], [-1, 1]])
b = np.array([[-1, 1], [1, -1]])

a @ b

Smarter Enums

Saving the hustle of not having to increment manually. PEP-435

from enum import Enum

class Color(Enum):
    Red = 1
    Green = 2
    Blue = 3
from enum import Enum, auto

class Color(Enum):
    Red = auto()
    Green = auto()
    Blue = auto()

Type Hinting

The most highly requested addition to the language, that helps not just intellisense suggestions, but also allows for more type safety-like features and easier code reading. You can access the function annotations dictionary using self.__annotations__. PEP-484

def func(name, age):
    return "{} is {} years old".format(name, age)
def func(name: str, age: int) -> str:
    return f"{name} is {age} years old"

More Flexible Iterable Unpacking

Spreading return values across variables has never been easier. Most modern languages already had this behaviour. Python is just catching up. PEP-3132

def unpack(a, b, *c):
    return a, b, c

zero, one, rest = unpack(*range(5))

# zero = 0
# one = 1
# rest = [2, 3, 4]
head, *body, tail = range(5)

# head = 0
# body = [1, 2, 3]
# tail = 4

zero, _, _, three, *_ = range(5)

# zero = 0
# three = 3

Better Namespace Recognition

In a nutshell, less __init__.py files across your folders. PEP-420

master_library/
       __init__.py
       sub_lib_1/
               __init__.py
               funcs.py
       sub_lib_2/
               __init__.py
               funcs.py
       sub_lib_3/
               __init__.py
               funcs.py
master_library/
       __init__.py
       sub_lib_1/
               funcs.py
       sub_lib_2/
               funcs.py
       sub_lib_3/
               funcs.py

Improved Object Declaration & Self-documentation

No longer, do we have to write out every init variable in the object constructor to store it inside the object. Also you get out-of-the-box __str__ declaration, so you have nice object printing. PEP-557

class Person:
    def __init__(self, name, weight, height):
        self.name = name
        self.weight = weight
        self.height = height
                 
    def bmi(self):
        return self.weight / self.height**2
    
ivo = Person("Ivo", 60, 1.8)

# <__main__.Person object at 0x0020C1A29ACC0>
from dataclasses import dataclass

@dataclass
class Person:
    name: str
    weight: float
    height: float
    
    def bmi(self) -> bool:
        return self.weight / self.height**2
    
ivo = Person("Ivo", 60, 1.8)

# Person(name='Ivo', weight=60, height=1.8)

Async / Await and Asyncio

The Asyncio was available from 3.3, but the decorator @asyncio.coroutine is now replaced with the async keyword and yield from with await. AsyncIO makes writing asynchronous code somewhat tolerable. PEP-492

# Not Available in Python 2.7
# The "Twisted" module as alternative
import asyncio

async def your_async_func():
   await asyncio.sleep(5)

asyncio.run(your_async_func())

New Integer Division Defaults

Since Python 3.0 the real floor operator // was introduced so the default behaviour of / was changed.

3/2
# 1

float(3/2)
# 1.5
3/2
# 1.5

3//2
# 1

Setting Breakpoints

If you are using the Python debugger (PDB), you can now write directly breakpoint(). PEP-553

import pdb;

function()
pdb.set_trace()
another_function()
function()
breakpoint()
another_function()

Context Variables

It’s variables that have different values according to their context. Behaving like Thread-Local storage, each thread can have a different value for the variable. Context variables allow for several contexts in the same OS thread. The strongest use case is for monitoring values across concurrent asynchronous function runs. PEP-567

from contextvars import ContextVar
import asyncio

my_var = ContextVar("my_var", default=True)

async def update_var():
   my_var.set(False)

async def print_var():
   print(my_var.get())

asyncio.run(update_var())
# Sets my_var to False

asyncio.run(print_var())
# True

Simplified Path Referencing

Working with directories has gotten a big readability bump when it comes to folder referencing. PEP-428

import os

dir = "/home"
file = os.path.join(dir, "abc.csv")

if os.path.exists(file):
    ...
from pathlib import Path

dir = Path("/home")
file = dir / "abc.csv"

if file.exists():
    ...

Meaningful Comparisons

No more random bugs because you forgot to cast a variable to the right type.

'ivo' > 999
# True
'ivo' > 999
# TypeError: '>' not supported between instances of 'str' and 'int'

Cleaner Inheritance Class Definition

Saving the confusion of the right order variable definition when defining an subclass. PEP-487

class DerivedClass(BaseClass):
    def __init__(self, name, **options):
        super(DerivedClass, self).__init__(name='subclass', **options)
class DerivedClass(BaseClass):
    def __init__(self, name, **options):
        super().__init__(name='subclass', **options)

More Explicit Numbers

Further to consolidating int and long into just int. You can now use underscores to make large numbers more readable. It works for floats, hex and binary numbers. PEP-515

n = 1000000.0
n = 1_000_000.0

Simpler Tree Globbing

Traversing deeply nested directories doesn’t have to be a pain. PEP-471

import glob

pictures = (
    glob.glob('/img/*.png')
  + glob.glob('/img/*/*.png')
  + glob.glob('/img/*/*/*.png'))
import glob

pictures = glob.glob('/img/**/*.png', recursive=True)

This was just a subset of the new additions, there were also Exception Chaining, which provides for better stacktraces, addition of clear() as a method to lists, a @lru_cache decorator for more efficient computing and many more. You can read the entire list of under-the-hood optimizations offered by Python 3.7 on the official Python Software Foundation page – here.


Leave a Reply