Python 3.9 officially landed on October 5th, right on schedule with the new annual release cadence. While most of the coverage is focused on the shiny new dictionary merge operator (|), there’s a lot more going on under the hood that deserves attention — particularly for those of us running Python in production environments.
Having tracked Python releases since the 2.x days, I can tell you that 3.9 feels like one of those “quietly important” releases. Not as dramatic as the 2-to-3 migration, but the kind of steady improvement that compounds over time.
The New PEG Parser#
The headline feature that nobody’s talking about is PEP 617 — the switch from the old LL(1) parser to a new PEG-based parser. On the surface, this changes nothing for existing code. Your scripts will parse and run identically. But this is a foundational change.
The LL(1) parser had real limitations. Grammar hacks and workarounds littered the CPython codebase because certain constructs simply couldn’t be expressed cleanly. The new PEG parser removes these constraints, which means future Python versions can introduce syntax that was previously impossible or impractical. Think of it as replacing the foundation of a house — the rooms look the same today, but you can now build additions that weren’t structurally possible before.
For Python 3.9, both parsers ship side by side, with PEG as the default. The old parser is still available via a flag for anyone who hits edge cases. That’s the kind of careful migration strategy I appreciate in a language that powers everything from data science notebooks to critical infrastructure.
Dictionary Merge and Update Operators#
Now, the feature everyone’s actually excited about — PEP 584. You can now merge dictionaries with | and update with |=:
config_defaults = {"timeout": 30, "retries": 3}
user_overrides = {"timeout": 60, "debug": True}
merged = config_defaults | user_overrides
# {'timeout': 60, 'retries': 3, 'debug': True}Is this a game-changer? Not really. We’ve had {**d1, **d2} since Python 3.5, and dict.update() since forever. But the new syntax is cleaner and more readable, especially when you’re chaining multiple merges. In configuration management code — which I write a lot of — this is a genuine quality-of-life improvement.
The |= update operator is particularly nice for in-place modifications without needing a separate method call. It follows the pattern established by sets, which already support | for union operations. Consistency in language design matters more than people think.
Type Hinting Gets Simpler#
PEP 585 is the change that’ll affect my daily coding the most. You can now use built-in collection types directly in type hints instead of importing from typing:
# Before
from typing import List, Dict, Tuple
def process(items: List[Dict[str, Tuple[int, ...]]]) -> None: ...
# Python 3.9
def process(items: list[dict[str, tuple[int, ...]]]) -> None: ...This might seem trivial, but when you’ve got a codebase where every module starts with from typing import ..., eliminating that boilerplate is welcome. More importantly, it makes type hints feel like a first-class citizen rather than an add-on library feature. I’ve been pushing type hints on my teams for the past two years, and any friction reduction helps adoption.
Timezone Support and String Methods#
Two smaller additions that deserve mention: PEP 615 adds IANA timezone support to the standard library via zoneinfo. No more reaching for pytz for basic timezone operations. As someone who’s debugged timezone-related production incidents more times than I care to admit, having this in stdlib is overdue.
The new str.removeprefix() and str.removesuffix() methods (PEP 616) also fill a gap that’s caused subtle bugs. I’ve seen too many uses of lstrip() where developers assumed it strips a prefix rather than a set of characters. The new methods do exactly what the name says, nothing more.
My Take: The Maturation Continues#
Python 3.9 isn’t flashy, and that’s exactly what I want from a language release in 2020. The Python core team has found a good rhythm with annual releases — each one delivers enough to be worth upgrading without breaking the world.
My main concern is the ongoing fragmentation in deployment environments. With Python 2 finally end-of-lifed in January, and now four maintained 3.x versions (3.6 through 3.9), library maintainers still carry significant compatibility burden. The deprecation of distutils in 3.9 and the ongoing packaging ecosystem improvements are steps in the right direction, but we’re not there yet.
For production environments, I’d recommend waiting for 3.9.1 (expected in December) before deploying. First point releases always catch the early bugs that only show up at scale. In the meantime, start testing your CI pipelines against 3.9 and update your type hints to use the new syntax — it’s backward-compatible with from __future__ import annotations.
The new parser is the real story here. It’s the kind of long-term investment that pays dividends over the next decade of Python development. The features in 3.10 and beyond will likely be more ambitious because of it.
