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.