Skip to content

What's New in Python 3.10

3.10

Python 3.10 was released in October 2021. It's the release that brought structural pattern matching — the most significant new syntax addition since walrus operators in 3.8. It also delivered dramatically better error messages.


Summary

Category Change
Language Structural pattern matching (match/case) — PEP 634
Language Parenthesized context managers
Language Better isinstance() with union types
Error Messages Much clearer SyntaxError, IndentationError messages
Typing X | Y union type syntax (PEP 604)
Typing TypeAlias, ParamSpec, TypeGuard
Standard Library zip(..., strict=True)

Structural Pattern Matching — PEP 634

This is Python's version of what other languages call switch/match, but much more powerful. You match not just values but structures.

command = input("Enter command: ")

match command.split():
    case ["quit"]:
        print("Quitting.")
    case ["go", direction]:
        print(f"Going {direction}.")
    case ["go", direction, speed]:
        print(f"Going {direction} at {speed} speed.")
    case ["pick", "up", item]:
        print(f"Picked up {item}.")
    case _:
        print(f"Unknown command: {command!r}")

You can match on class instances too:

from dataclasses import dataclass

@dataclass
class Point:
    x: float
    y: float

def describe(point: Point) -> str:
    match point:
        case Point(x=0, y=0):
            return "origin"
        case Point(x=0, y=y):
            return f"on y-axis at {y}"
        case Point(x=x, y=0):
            return f"on x-axis at {x}"
        case Point(x=x, y=y):
            return f"at ({x}, {y})"

See the full Pattern Matching guide for all pattern types: literal, capture, wildcard, OR, AS, guard, sequence, mapping, and class patterns.


Union Types with | — PEP 604

Previously you had to write Union[int, str] from typing. Now you can use | directly:

# Old way
from typing import Union, Optional
def process(value: Union[int, str]) -> Optional[str]:
    ...

# New in 3.10
def process(value: int | str) -> str | None:
    ...

This also works with isinstance() and issubclass():

isinstance(42, int | str)  # True
isinstance("hello", int | str)  # True
isinstance(3.14, int | str)  # False

Parenthesized Context Managers

Multi-line with statements are now legal with parentheses:

# Old — need backslash continuation
with open("a.txt") as a, \
     open("b.txt") as b:
    ...

# New in 3.10 — clean parentheses
with (
    open("a.txt") as a,
    open("b.txt") as b,
    open("c.txt") as c,
):
    ...

Better Error Messages

3.10 introduced a big improvement in SyntaxError and IndentationError messages. Examples:

# Forgetting closing bracket
my_list = [1, 2, 3
            ^
SyntaxError: '[' was never closed

# Common dictionary mistake
d = {"key": "value" "oops": 1}
                    ^^^^^^
SyntaxError: invalid syntax. Perhaps you forgot a comma?

zip(..., strict=True)

Catches a common bug: zipping iterables of different lengths silently truncates to the shortest. With strict=True, it raises ValueError instead:

names = ["Alice", "Bob", "Carol"]
scores = [95, 87]  # One element missing!

# Old behavior — silently drops "Carol"
list(zip(names, scores))  # [('Alice', 95), ('Bob', 87)]

# New in 3.10 — raises ValueError
list(zip(names, scores, strict=True))
# ValueError: zip() has arguments with different lengths