Python 3.9: New Union Operators

python dev

Python 3.9 shipped in October 2020 with quality-of-life improvements. The headline: dictionary merge operators. Also: type hinting improvements and string methods.

Dictionary Union Operators

The Merge Operator (|)

# Before Python 3.9
merged = {**dict1, **dict2}
# or
merged = dict1.copy()
merged.update(dict2)

# Python 3.9+
merged = dict1 | dict2

Clean, readable, obvious.

The Update Operator (|=)

# Before
dict1.update(dict2)

# Python 3.9+
dict1 |= dict2

Modifies dict1 in place.

Behavior

Right-hand side wins for duplicate keys:

a = {'x': 1, 'y': 2}
b = {'y': 3, 'z': 4}

print(a | b)  # {'x': 1, 'y': 3, 'z': 4}
print(b | a)  # {'y': 2, 'z': 4, 'x': 1}

Works with Other Mappings

from collections import ChainMap, Counter

# With ChainMap
c = ChainMap({'a': 1}) | {'b': 2}

# With Counter
c1 = Counter(a=3, b=1)
c2 = Counter(a=1, b=2)
c1 | c2  # Counter({'a': 3, 'b': 2}) - max for each key

Type Hinting Improvements

Use Built-in Types Directly

# Before Python 3.9
from typing import List, Dict, Tuple

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

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

No more importing from typing for basic types.

Supported Generics

# All of these work now
list[str]
dict[str, int]
tuple[int, ...]
set[float]
frozenset[str]

# Also complex types
list[dict[str, list[int]]]

Still Need typing For

from typing import Optional, Union, Callable, TypeVar

# These still come from typing
Optional[str]  # Same as str | None in 3.10+
Union[int, str]
Callable[[int], str]

String Methods

removeprefix() and removesuffix()

# Before
filename = "test_user.py"
if filename.startswith("test_"):
    filename = filename[5:]
if filename.endswith(".py"):
    filename = filename[:-3]

# Python 3.9+
filename = "test_user.py"
filename = filename.removeprefix("test_")  # "user.py"
filename = filename.removesuffix(".py")    # "user"

Difference from strip()

text = "HelloWorld"

# strip removes *characters* from both ends
text.strip("Held")  # "oWor" - removes H, e, l, d

# removeprefix removes *prefix*
text.removeprefix("Hello")  # "World"

Other Improvements

zoneinfo Module

from zoneinfo import ZoneInfo
from datetime import datetime

# No more pytz needed for basic timezone work
dt = datetime(2020, 10, 5, tzinfo=ZoneInfo("America/New_York"))
print(dt)  # 2020-10-05 00:00:00-04:00

# Convert timezones
dt_london = dt.astimezone(ZoneInfo("Europe/London"))

graphlib.TopologicalSorter

from graphlib import TopologicalSorter

# Dependencies
graph = {
    "D": {"B", "C"},
    "C": {"A"},
    "B": {"A"}
}

ts = TopologicalSorter(graph)
print(list(ts.static_order()))  # ['A', 'C', 'B', 'D']

Useful for build systems, task scheduling.

Relaxed Decorator Syntax

# Before: Only simple expressions
@decorator
@module.decorator
@decorator(args)

# Python 3.9+: Any valid expression
@buttons[0].clicked.connect
def on_click():
    pass

@(lambda f: f)
def identity():
    pass

PEG Parser

Python’s parser was rewritten from LL(1) to PEG. Mostly invisible but enables:

Migration Notes

Minimum Version

Many libraries still support Python 3.7/3.8. Check compatibility.

Type Hints Compatibility

# For code that needs to run on older Python
from __future__ import annotations

# Now this works even on 3.7+
def process(items: list[str]) -> dict[str, int]:
    ...

Check Dictionary Merge Usage

# These are NOT equivalent
a | b  # Creates new dict
a |= b  # Modifies a in place

# Be careful with aliasing
c = a
a |= b  # c is also modified!

Upgrade Path

From 3.8:

  1. Update Python: pyenv install 3.9.0
  2. Run tests
  3. Optionally update type hints to use built-in generics
  4. Optionally use dict operators

Minimal breaking changes for 3.8 → 3.9.

Final Thoughts

Python 3.9 is about polish. Dictionary operators make common patterns cleaner. Type hints get simpler. String methods fill gaps.

No revolution, but meaningful improvements. Upgrade when your dependencies support it.


Small improvements compound. Keep updating.

All posts