Python 3.9 is officially out, and while it doesn’t have the dramatic headline of Python 2’s end-of-life (RIP, January 2020), it’s packed with quality-of-life improvements that reflect where the language is heading. This is the first version to drop support for Windows 7, the first built with a new parser, and it brings some syntactic sugar that I’ve been wanting for years.
Let me walk through what matters.
Dictionary Merge and Update Operators#
This is the one I’m most excited about. Python 3.9 introduces the | operator for merging dictionaries and |= for updating them in place:
# Merging dictionaries — creates a new dict
defaults = {"color": "blue", "size": "medium"}
overrides = {"size": "large", "weight": "heavy"}
config = defaults | overrides
# {'color': 'blue', 'size': 'large', 'weight': 'heavy'}
# Update in place
defaults |= overridesBefore this, you had several options, none of them great:
# The unpacking approach (3.5+)
config = {**defaults, **overrides}
# The copy-and-update approach
config = defaults.copy()
config.update(overrides)
# The ChainMap approach
from collections import ChainMap
config = dict(ChainMap(overrides, defaults))The unpacking syntax {**a, **b} works but isn’t obvious to newcomers, and it doesn’t generalize to update-in-place. The | operator is clean, readable, and consistent with set operations that already use the same symbol. It’s one of those changes where you wonder why it took so long — and then you read the PEP discussions and remember that Python’s design process is thorough to a fault.
Type Hinting Gets Simpler#
PEP 585 brings a change I’ve been looking forward to: you can now use built-in collection types directly in type hints instead of importing from typing:
# Before (still works, but verbose)
from typing import List, Dict, Tuple
def process(items: List[str]) -> Dict[str, Tuple[int, int]]:
...
# Python 3.9
def process(items: list[str]) -> dict[str, tuple[int, int]]:
...No more from typing import List, Dict, Set, Tuple, FrozenSet. You just use list, dict, set, tuple directly with subscripts. The typing module equivalents are now deprecated (though they’ll work for the foreseeable future).
This pairs nicely with PEP 604, which lets you write X | Y instead of Union[X, Y]:
# Before
from typing import Union
def fetch(id: Union[int, str]) -> Union[dict, None]:
...
# Python 3.9+
def fetch(id: int | str) -> dict | None:
...These changes reduce the boilerplate tax of type hints significantly. I’ve seen teams resist adopting type hints partly because of the import overhead and verbose syntax. These improvements remove that excuse.
The New PEG Parser#
Under the hood, Python 3.9 ships with a completely new parser based on Parsing Expression Grammar (PEG), replacing the LL(1) parser that Python has used since its inception. The old parser is still available via -X oldparser but is scheduled for removal in 3.10.
For most developers, this change is invisible — existing syntax parses exactly the same way. The significance is in what it enables for future versions. The LL(1) parser had fundamental limitations that made certain syntax proposals impossible. The PEG parser doesn’t share those constraints, which opens the door for more expressive syntax in future Python releases.
The Python core team has been careful to note that the new parser produces identical results for all existing Python code. This is a foundation change, not a feature change — its impact will be felt in future releases, not this one.
String Methods and Time Zone Support#
Two smaller additions worth mentioning:
str.removeprefix() and str.removesuffix() are finally here. If you’ve ever written this pattern:
if filename.startswith("test_"):
name = filename[len("test_"):]You can now write:
name = filename.removeprefix("test_")It’s safer (no off-by-one on the slice length) and more readable. I’ve had helper functions for this in my personal utility libraries for years. Nice to see it in the standard library.
On the time zone front, the zoneinfo module brings IANA time zone support into the standard library. No more reaching for pytz for basic time zone handling:
from zoneinfo import ZoneInfo
from datetime import datetime
dt = datetime.now(tz=ZoneInfo("Europe/Amsterdam"))As someone based in the Netherlands, I appreciate not needing a third-party package just to correctly represent my local time.
My Take#
Python 3.9 is not a revolutionary release, and that’s perfectly fine. The language is in a phase where incremental refinement serves the community better than dramatic changes. The dictionary operators and simplified type hints are exactly the kind of improvements that make daily coding a little smoother without requiring anyone to relearn the language.
The PEG parser is the most strategically important change, even though you won’t notice it today. It’s an investment in Python’s future expressiveness, and I’m curious to see what syntax proposals emerge now that the parser is no longer the bottleneck.
If you’re still on 3.7 or 3.8, there’s no urgent reason to upgrade immediately — but start testing your codebases against 3.9 now. The typing improvements alone are worth the move for any team that’s invested in type hints. And those dictionary merge operators? They’re going to become second nature faster than you’d expect.
Python continues its steady march forward. No hype, no drama, just consistent improvement. After thirty years of watching programming languages come and go, I can tell you that’s exactly the recipe for longevity.
