Overview

Python 3.9, released on October 5, 2020, brings highly anticipated improvements that simplify everyday code. Dictionary union operators, type hints using built-in types directly, the zoneinfo module for time zones, and the removeprefix/removesuffix string methods are all new features that make Python more expressive and practical.

This release also marks the adoption of a new annual release cycle: from now on, a new major Python version ships every October.

Major Features

Dictionary union operators (PEP 584)

Python 3.9 introduces the | and |= operators for merging dictionaries. The | operator creates a new dictionary from two existing ones, while |= updates a dictionary in place. When keys overlap, values from the right-hand dictionary win.

python
# Merging two dictionaries with |
default_config = {"theme": "light", "language": "en", "notifications": True}
user_config = {"theme": "dark", "font_size": 14}

final_config = default_config | user_config
print(final_config)
# {'theme': 'dark', 'language': 'en', 'notifications': True, 'font_size': 14}

# In-place update with |=
inventory = {"apples": 5, "bananas": 3, "oranges": 8}
delivery = {"apples": 10, "kiwis": 6}

inventory |= delivery
print(inventory)
# {'apples': 10, 'bananas': 3, 'oranges': 8, 'kiwis': 6}

# Practical case: multi-layer configuration merge
env_default = {"debug": False, "port": 8000, "host": "localhost"}
env_dev = {"debug": True, "log_level": "DEBUG"}
env_local = {"port": 9000}

# Chaining merges: each layer overrides the previous one
config = env_default | env_dev | env_local
print(config)
# {'debug': True, 'port': 9000, 'host': 'localhost', 'log_level': 'DEBUG'}

Type hinting with built-in generics (PEP 585)

Before Python 3.9, you had to import List, Dict, Tuple, etc. from the typing module to annotate generic types. Now you can use list, dict, tuple, and other built-in types directly as generic annotations. This greatly simplifies imports and code readability.

python
# Before Python 3.9:
# from typing import List, Dict, Tuple, Optional
# def process_grades(grades: List[int]) -> Dict[str, float]:

# Python 3.9: built-in types are enough
def compute_statistics(grades: list[int]) -> dict[str, float]:
    """Compute statistics for a list of grades."""
    return {
        "average": sum(grades) / len(grades),
        "minimum": float(min(grades)),
        "maximum": float(max(grades)),
    }

def find_duplicates(items: list[str]) -> set[str]:
    """Identify items that appear more than once."""
    seen = set()
    duplicates = set()
    for item in items:
        if item in seen:
            duplicates.add(item)
        seen.add(item)
    return duplicates

def index_products(products: list[tuple[str, float]]) -> dict[str, float]:
    """Transform a list of pairs into a dictionary."""
    return {name: price for name, price in products}

# Usage
stats = compute_statistics([12, 15, 18, 9, 14])
print(stats)  # {'average': 13.6, 'minimum': 9.0, 'maximum': 18.0}

duplicates = find_duplicates(["alice", "bob", "alice", "charlie", "bob"])
print(duplicates)  # {'alice', 'bob'}

zoneinfo module (PEP 615)

The zoneinfo module provides direct access to the IANA time zone database without needing to install pytz. This is now the recommended way to handle time zones in Python.

python
from datetime import datetime, timedelta
from zoneinfo import ZoneInfo

# Create timezone-aware datetimes
paris = ZoneInfo("Europe/Paris")
new_york = ZoneInfo("America/New_York")
tokyo = ZoneInfo("Asia/Tokyo")

meeting_paris = datetime(2024, 3, 15, 14, 0, tzinfo=paris)
print(f"Paris     : {meeting_paris.strftime('%H:%M %Z')}")
# Paris     : 14:00 CET

# Convert between time zones
meeting_ny = meeting_paris.astimezone(new_york)
meeting_tokyo = meeting_paris.astimezone(tokyo)
print(f"New York  : {meeting_ny.strftime('%H:%M %Z')}")
# New York  : 08:00 EST
print(f"Tokyo     : {meeting_tokyo.strftime('%H:%M %Z')}")
# Tokyo     : 22:00 JST

# Scheduling with timezone handling
def schedule_reminder(local_time, timezone_name, delay_hours):
    """Schedule a reminder in a given time zone."""
    now = datetime.now(tz=ZoneInfo(timezone_name))
    reminder = now + timedelta(hours=delay_hours)
    return reminder

reminder = schedule_reminder("14:00", "Europe/Paris", 2)
print(f"Reminder at: {reminder.strftime('%H:%M %Z')}")

str.removeprefix() and str.removesuffix() (PEP 616)

These two new methods allow removing a prefix or suffix from a string in a readable and safe way. Unlike manual slicing with lstrip/rstrip (which operate character by character), these methods work on entire substrings.

python
# Cleaning up URLs
urls = [
    "https://www.example.com/api/v2/users",
    "https://www.example.com/api/v2/products",
    "https://www.example.com/api/v2/orders",
]

base = "https://www.example.com/api/v2/"
endpoints = [url.removeprefix(base) for url in urls]
print(endpoints)  # ['users', 'products', 'orders']

# Processing file names
files = ["report_2024.csv", "data_2024.csv", "archive_2024.csv.bak"]

# Remove the .csv extension
without_ext = [f.removesuffix(".csv") for f in files]
print(without_ext)
# ['report_2024', 'data_2024', 'archive_2024.csv.bak']
# Note: archive_2024.csv.bak is unchanged since it doesn't end with .csv

# Removing test prefix from method names
methods = ["test_login", "test_logout", "helper_create"]
names = [m.removeprefix("test_") for m in methods]
print(names)  # ['login', 'logout', 'helper_create']

Minor Improvements

  • The new PEG parser replaces the historical LL(1) parser (PEP 617), paving the way for future syntax improvements.
  • math.gcd() now accepts an arbitrary number of arguments.
  • The graphlib module is added with TopologicalSorter for topological sorting.
  • Decorators now accept any valid expression (PEP 614).
  • random.randbytes() is added for generating random bytes.

Deprecations and Removals

Notable changes in this category:

  • The release cycle switches to an annual cadence (PEP 602).
  • Several rarely used modules are deprecated and scheduled for removal in Python 3.12 (PEP 594): aifc, audioop, cgi, imghdr, etc.
  • random.shuffle() no longer supports the random parameter.

Sources