Python 3.9: What's New

Last updated October 9th, 2020

Python 3.9, which was released on October 5th 2020, doesn't bring any major new features to the table, but there are still some significant changes especially to how the language is developed and released.


Development Cycle

This is the first Python release coming from the new release philosophy, where major releases happen every 12-months (in October) rather than every 18-months. With releases being more frequent, changes should be smaller -- which is exactly what we're seeing in Python 3.9.

All new versions have 1.5 years of full support along with 3.5 years of security fixes.

Python release calendar

More info:

Dictionary Unions

Merge dictionaries

We now have a merge operator for performing dictionary unions: |. It works the same as a.update(b) or {**a, **b}, with one difference: It works for any instance of the dict subclass.

If you have Docker, you can quickly spin up a Python 3.9 shell to play around with the examples in this post via: docker run -it --rm python:3.9.

You can merge two dictionaries like so:

user = {'name': 'John', 'surname': 'Doe'}
address = {'street': 'Awesome street 42', 'city': 'Huge city', 'post': '420000'}

user_with_address = user | address

# {'name': 'John', 'surname': 'Doe', 'street': 'Awesome street 42', 'city': 'Huge city', 'post': '420000'}

Now, we have a new dictionary called user_with_address, which is a union of user and address.

If there are duplicate keys in the dictionaries, then the output will display the second (rightmost) key-value pair:

user_1 = {'name': 'John', 'surname': 'Doe'}
user_2 = {'name': 'Joe', 'surname': 'Doe'}

users = user_1 | user_2
# {'name': 'Joe', 'surname': 'Doe'}

| operator means union, not or. It doesn't do any bitwise work nor does it serve as logical operator. It's used to create a union of two dictionaries. Since it looks similar to an or condition in other languages, you may want to double check how it's being used during code reviews.

Updating dictionaries

As for updating, you now have the operator |=. This works in place.

You can update the first dictionary with keys and values from the second like so:

grades = {'John': 'A', 'Marry': 'B+'}
grades_second_try = {'Marry': 'A', 'Jane': 'C-', 'James': 'B'}

grades |= grades_second_try

# {'John': 'A', 'Marry': 'A', 'Jane': 'C-', 'James': 'B'}

It works for anything with keys and __getitem__ or iterables with key-value pairs:

# example 1
grades = {'John': 'A', 'Marry': 'B+'}
grades_second_try = [('Marry', 'A'), ('Jane', 'C-'), ('James', 'B')]
grades |= grades_second_try
# {'John': 'A', 'Marry': 'A', 'Jane': 'C-', 'James': 'B'}

# example 2
x = {0: 0, 1: 1}
y = ((i, i**2) for i in range(2,6))
x |= y
{0: 0, 1: 1, 2: 4, 3: 9, 4: 16, 5: 25}

# example 3
x | y
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unsupported operand type(s) for |: 'dict' and 'generator'

More info:

Generating Random Bytes

The random library can now be used to generate random bytes via randbytes.

For example, to generate ten random bytes:

import random


More info:

String Methods

Two methods were added to the str object:

  1. removeprefix
  2. removesuffix


This first method removes the inputted string from the beginning of another string.

For example:

file_name = 'DOCUMENT_001.pdf'

#  001.pdf

If the string doesn't start with the input string, a copy of the original string will be returned:

file_name = 'DOCUMENT_001.pdf'

# DOCUMENT_001.pdf


Similarly, we can remove the suffix from the selected string with the second method.

To remove the .pdf file extension from the file name:

file_name = 'DOCUMENT_001.pdf'


file_name = 'DOCUMENT_001.pdf'

# DOCUMENT_001.pdf

More info:

IANA Timezone Support

The zoneinfo module has been added to support the IANA time zone database.

For example, to create a time zone–aware timestamp, you can add the tz or tzinfo arguments to the datetime method:

import datetime
from zoneinfo import ZoneInfo

datetime.datetime(2020, 10, 7, 1, tzinfo=ZoneInfo('America/Los_Angeles'))
# datetime.datetime(2020, 10, 7, 1, 0, tzinfo=zoneinfo.ZoneInfo(key='America/Los_Angeles'))

You can easily convert between time zones as well:

import datetime
from zoneinfo import ZoneInfo

start = datetime.datetime(2020, 10, 7, 1, tzinfo=ZoneInfo('America/Los_Angeles'))
datetime.datetime(2020, 10, 7, 9, 0, tzinfo=zoneinfo.ZoneInfo(key='Europe/London'))

More info:

Generic Type Annotations

From now on you can use generic types for type annotations. Instead of having to use typing.List or typing.Dict, you can use the list or dict built-in collection types as generic types

def sort_names(names: list[str]):
    return sorted(names)

With Python being dynamically typed, types are dynamically inferred. Since this isn't always desireable, type hinting can be used to specify types. This was introduced way back in Python 3.5. Being able to use built-in collection types as generic types greatly simplifies type hinting.

More info:

Canceling Concurrent Futures

A new parameter called cancel_futures has been added to concurrent.futures.Executor.shutdown(). When set to True, it cancels all pending futures that have not started running. Prior to version 3.9, the process would wait for them to complete before shutting down the executor.

More info:


In previous versions __import__ and importlib.util.resolve_name() raised ValueError when relative import went past its top-level package. Now you'll get an ImportError, which better describes the condition being handled.

More info:

String Replace Fix

An issue with string replace for empty strings has been fixed.

In previous versions:

"".replace("", "prefix", 1)
# ''

From now on:

"".replace("", "prefix", 1)
# 'prefix'

More info:

New Parser

A new, more flexible, parser based on PEG (Parsing expression grammar) has been introduced. Although you probably won't notice it, this is the most significant change for this release of Python.

The PEG-based parser's performance is comparable to the old old LL(1) (Left-to-right parser) but it's more flexible formalism should make it easier to design new language features.

More info:


Finally, the vectorcall protocol, which was introduced in Python 3.8, has now been extended to several built-ins, including range, tuple, set, frozenset, list, and dict. In short, vectorcall makes many common function calls faster by removing the overhead from reducing the number of temporary objects created for the call.

More info:


This post touched on just the major changes to the language. A full list of changes can be found here.

Happy coding!

Jan Giacomelli

Jan Giacomelli

Jan is a software engineer who lives in Ljubljana, Slovenia, Europe. He is a Staff Software Engineer at where he is leading backend engineering efforts. He loves Python, FastAPI, and Test-Driven Development. When he's not writing code, deploying to AWS, or speaking at a conference, he's probably skiing, windsurfing, or playing guitar. Currently, he's working on his new course Complete Python Testing Guide.

Share this tutorial

Featured Course

Building Your Own Python Web Framework

In this course, you'll learn how to develop your own Python web framework to see how all the magic works beneath the scenes in Flask, Django, and the other Python-based web frameworks.

Featured Course

Building Your Own Python Web Framework

In this course, you'll learn how to develop your own Python web framework to see how all the magic works beneath the scenes in Flask, Django, and the other Python-based web frameworks.