What’s new in Python 3.10

The latest version of Python sports powerful pattern matching features, better error reporting, and smarter typing syntax for wrapped functions.

What’s new in Python 3.10
Getty Images

Python 3.10, the latest in-development version of Python, has been released. Intrepid Python developers are encouraged to test their code against it, with proper precautions (e.g., using a virtual environment).

There aren’t many truly new major features in Python 3.10, but of the few that we do have, one of them — structural pattern matching — may be the single most significant addition to the language syntax since async.

Here’s a rundown of all of the major new features in Python 3.10, with discussion of how they can help your code.

Structural pattern matching

An outgrowth of previous failed attempts to add a switch/case-like syntax to Python, structural pattern matching lets you match variables against one of a set of possible values (as with switch/case in other languages). But it also allows you to match against patterns of values — e.g., an object with a certain property set to a certain value. This greatly expands the range of possibilities, and makes it possible to write code that quickly encompasses a variety of scenarios. For example:

command = input()
match command.split():
case ["quit"]:
quit()
case ["load", filename]:
load_from(filename)
case ["save", filename]:
save_to(filename)
case _:
print (f"Command '{command}' not understood")

For more details on how to use pattern matching, see our how-to on this new syntax.

More precise error reporting

Python’s error reporting has long been at the mercy of the whims of its parser. Python 3.9 rolled out an entirely new parser — faster, more robust, easier for the Python team to maintain, and less riddled with internal hacks.

One big bonus the new parser offers developers is far more precise and useful error messages. In Python 3.8, the following code would generate a syntax error.

print ("Hello"
print ("What's going on?")

File ".\test.py", line 2
print ("What's going on?")
^
SyntaxError: invalid syntax

Not very helpful, because the real problem is one line earlier. Python 3.10 generates a far more useful error:

  File ".\test.py", line 1
print ("Hello"
^
SyntaxError: '(' was never closed

Many of the errors produced by the parser have been improved in this vein — not only delivering more precise information about the error, but more precise information about where the error actually occurs.

Parameter specification variables

Python’s typing module, used to annotate code with type information, lets you describe the types of a callable (e.g., a function). But that type information can’t be propagated across callables. This makes it hard to annotate things like function decorators.

Two new additions to typing, typing.ParamSpec and typing.Concatenate, make it possible to annotate callables with more abstract type definition information.

Here is an example taken from the PEP document on this new feature.

from typing import Awaitable, Callable, TypeVar

R = TypeVar("R")

def add_logging(f: Callable[..., R]) -> Callable[..., Awaitable[R]]:
  async def inner(*args: object, **kwargs: object) -> R:
    await log_to_database()
    return f(*args, **kwargs)
  return inner

@add_logging
def takes_int_str(x: int, y: str) -> int:
  return x + 7

await takes_int_str(1, "A")
await takes_int_str("B", 2) # fails at runtime

Because it isn’t possible to provide the linter with proper details about what kinds of types are being passed to the functions that are processed by the decorator, the linter can’t catch the invalid types in the second instance of takes_int_str.

Here’s how this code would work with the new parameter specification variable syntax.

from typing import Awaitable, Callable, ParamSpec, TypeVar

P = ParamSpec("P")
R = TypeVar("R")

def add_logging(f: Callable[P, R]) -> Callable[P, Awaitable[R]]:
  async def inner(*args: P.args, **kwargs: P.kwargs) -> R:
    await log_to_database()
    return f(*args, **kwargs)
  return inner

@add_logging
def takes_int_str(x: int, y: str) -> int:
  return x + 7

await takes_int_str(1, "A") # Accepted
await takes_int_str("B", 2) # Correctly rejected by the type checker

ParamSpec lets us indicate where to capture positional and keyword arguments. Concatenate can be used to indicate how arguments are added or removed, something commonly done with decorators.

Other major changes in Python 3.10

  • Union types can now be expressed as X|Y, instead of Union[X,Y], for brevity (PEP 604).
  • The zip built-in, which braids together the results of multiple iterables, now has a strict keyword. When set to True, it causes zip to raise an exception if one of the iterables is exhausted before the others (PEP 618).
  • with statements now support multi-line, parenthetical syntax (BPO-12782).
  • Variables can now be declared as type aliases, to allow forward references, more robust errors involving types, and better distinctions between type declarations in scopes (PEP 613).
  • OpenSSL 1.1.1 or newer is now required to build CPython. This modernizes one of CPython’s key dependencies (PEP 644).

Copyright © 2021 IDG Communications, Inc.