Alright, let’s talk about Python 3.9. This is where the core devs finally looked at some of our most common, grunt-work code patterns and said, “Yeah, we can fix that.” It’s a release that feels less like a grand new vision and more like a highly competent engineer finally getting around to the quality-of-life improvements we’d all been begging for. Two features stand out: dictionary union operators and the ability to use generics in standard collection type hints. Both are deceptively simple-looking changes that will save you from a surprising amount of boilerplate and outright ugliness.

The Dict Union Operators: Finally, A Sane Way to Merge

For years, merging two dictionaries was a rite of passage. You’d learn about dict.update(), which modifies the original dict in-place (rude), and then you’d learn the classic {**d1, **d2} pattern, which is about as elegant as a soup sandwich. It works, but you have to squint to remember which dictionary takes precedence if there’s a key collision (it’s the one on the right, by the way).

Python 3.9 said, “Enough.” and gave us the | union operator and the |= update operator.

# The Old Ways (R.I.P.)
default_config = {"theme": "dark", "language": "en"}
user_config = {"language": "fr", "accessibility": True}

# The clunky way that creates a new dict
merged_config = {**default_config, **user_config}
print(merged_config)  # {'theme': 'dark', 'language': 'fr', 'accessibility': True}

# The in-place way that ruins your original dict
default_config_copy = default_config.copy()
default_config_copy.update(user_config)

# The New, Glorious Way
merged_config = default_config | user_config  # Clean, readable, new dict.
print(merged_config)  # Same output as above.

# And for in-place updating, because sometimes you *are* a barbarian:
default_config |= user_config  # Now default_config is permanently altered.
print(default_config)  # {'theme': 'dark', 'language': 'fr', 'accessibility': True}

The reason this is so brilliant is that it makes the code’s intent immediately obvious. You see |, you think “union.” You see |=, you think “update.” No more deciphering double-asterisk voodoo. The designers got this one unequivocally right.

Watch Your Step: The precedence of | isn’t as high as you might hope. When mixing with other operators, always use parentheses. Don’t be a hero.

# This will bite you:
result = x = {} | {} if True else {}  # SyntaxError: invalid syntax. Wait, what?

# Do this instead. Just use the parentheses. Trust me.
result = (x := {} | {}) if True else {}

Typing: Generics in Standard Collections

Before 3.9, type hinting for lists, dictionaries, and other built-in collections was a mess of importing capital-letter types from the typing module. It was verbose, it looked out of place, and it felt like the type system was bolted on as an afterthought (because, well, it was).

# The Old Verbosity (from Python 3.5 - 3.8)
from typing import List, Dict, Tuple

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

With Python 3.9, you can use the built-in collection types as generic types directly. The interpreter now understands that list[str] means exactly the same thing as typing.List[str].

# The New, Cleaner Way (Python 3.9+)
def process_data(items: list[str]) -> dict[str, tuple[int, int]]:
    ...

Why this matters: It eliminates a whole class of import statements, making your code cleaner. More importantly, it makes generic types feel like a first-class citizen of the language instead of a weird add-on library. The syntax is consistent and intuitive. It’s one of those changes that seems minor but significantly lowers the barrier to writing well-typed code.

The Rough Edge: You have to be on Python 3.9 or newer for this to work. If your codebase needs to support older versions, you’re stuck with the from typing import ... ceremony for a while longer. This is the migration pain point. There’s no way around it. My advice? Use the new syntax everywhere and let your linting tool (like mypy with the --python-version flag) handle the compatibility checks for you. The new syntax is just better, so it’s worth moving toward.

In summary, 3.9 gave us two features that are all about removing friction. They don’t introduce wild new concepts; they just make the existing, everyday things dramatically less annoying. And that, frankly, is the mark of a mature language. It’s not just about what it can do, but how gracefully it lets you do it.