Skip to content

What's New in Python 3.9

3.9

Python 3.9 released in October 2020. This release is significant for one reason most developers notice immediately: you can finally use list[int] and dict[str, int] as type hints without importing from typing. Plus the handy str.removeprefix() and str.removesuffix() methods.


Summary

Category Change
Language Built-in generics in type hints (PEP 585)
Language dict merge (|) and update (|=) operators (PEP 584)
Standard Library str.removeprefix() and str.removesuffix()
Standard Library zoneinfo module — IANA time zone database
Standard Library graphlib — topological sort
Standard Library math.gcd() accepts multiple arguments
CPython New parser (PEG-based, PEP 617)

Built-in Generic Types in Annotations — PEP 585

Before 3.9, if you wanted to type-hint a list of integers, you needed to import List from typing:

# Python 3.8 and earlier
from typing import List, Dict, Tuple, Set

def process(items: List[int]) -> Dict[str, List[int]]:
    ...

Now you use the built-in types directly:

# Python 3.9+
def process(items: list[int]) -> dict[str, list[int]]:
    ...

# Works for all built-in generics:
def parse(raw: bytes) -> tuple[int, str, list[float]]:
    ...

def unique(items: set[str]) -> frozenset[str]:
    ...

From __future__ for older Python

If you need your code to run on 3.7–3.8 but want the new syntax, add from __future__ import annotations at the top of the file. This makes all annotations lazy strings, so the new syntax is accepted at parse time.


Dict Merge Operators — PEP 584

defaults = {"color": "red", "size": "M"}
overrides = {"color": "blue", "weight": 1.5}

# Merge — creates a new dict
merged = defaults | overrides
print(merged)  # {'color': 'blue', 'size': 'M', 'weight': 1.5}

# Update in place
defaults |= overrides
print(defaults)  # {'color': 'blue', 'size': 'M', 'weight': 1.5}

The right-side dict takes priority for duplicate keys. This replaces the common {**a, **b} pattern with cleaner syntax.


str.removeprefix() and str.removesuffix()

Two very commonly needed operations that previously required an awkward lstrip() or manual slicing:

filename = "test_my_function.py"

# Remove test_ prefix if present
name = filename.removeprefix("test_")
print(name)  # my_function.py

# Remove .py suffix if present
base = filename.removesuffix(".py")
print(base)  # test_my_function

# If prefix/suffix isn't present, the original string is returned unchanged
print("hello".removeprefix("nope"))  # hello

Not the same as lstrip()

lstrip() strips any combination of the given characters, not a specific prefix string. removeprefix() removes the exact string once. They behave very differently.


zoneinfo — Real Timezone Support

The old datetime.timezone.utc and manual offset approach for timezones was limited. zoneinfo brings the full IANA timezone database into the standard library:

from datetime import datetime
from zoneinfo import ZoneInfo

# Create timezone-aware datetime
dt_utc = datetime(2024, 3, 1, 12, 0, tzinfo=ZoneInfo("UTC"))
dt_ist = dt_utc.astimezone(ZoneInfo("Asia/Kolkata"))
print(dt_ist)  # 2024-03-01 17:30:00+05:30

dt_ny = dt_utc.astimezone(ZoneInfo("America/New_York"))
print(dt_ny)   # 2024-03-01 07:00:00-05:00

No more manually tracking UTC offsets or installing pytz.