Python 3.10

Python 3.10 is out (and has been for a while, I’m late posting this), with new features and changes. The big new language feature this update is pattern matching. We get much better errors, the always-present typing improvements, and finally some real usage of the new PEG parser from 3.9.


Posts in the Python Series2→3 3.7 3.8 3.9 3.10 3.11 3.12 3.13

Official release image

Better Errors

This is the main reason you should be using 3.10+ to run your code, even if you still support older versions. Better error messages makes debugging code easier automatically, without needing libraries to start using some new feature!

Some key improvements are:

  • Attribute errors / name errors now include a “did you mean?” suggestion if a similar attribute exists (using difflib).
  • Unclosed structures now point at the start of the unclosed section, instead of at the first invalid syntax caused by the missing closer (quote or bracket).
  • Syntax errors now show the entire broken syntax segment, rather than just the start.
  • Many syntax errors are now are more specifically worded for the error they caught.
  • Indentation errors include more context.

In addition to this, CPython now provides more precise line numbers for debuggers and other tools.

Pattern Matching

This is a huge feature and the first new block type since with blocks (2.6). It is also the first nested block type in Python. It’s a huge new feature, so let’s just look at a small example:

# Load pyproject using tomli (or tomllib in Python 3.11)

match pyproject:
    case {"tool": {"mypy": {"strict": value}}}:
        print(f"You have strict mode set to {value}, congrats!")
    case _:
        print("Ahh, buggers. No strict mode for you.")

This may look a bit like a switch statement (and that’s a subset), but it also has several other features, some of which you see above. First, it can match against structure, such as collections and mappings; this can be nested as well. Second, while it can compare values by equality, it can also bind variables (value above) wherever they occur in the structure. Third, it matches top to bottom (and only one), so you can add a final case _ if you want.

A few features not listed above include the ability to add guards (final if statements), match a union of several things (denoted with |), and capture a portion explicitly with as. One more feature should be mentioned explicitly: support for classes, which can replace isinstance. match x: case Thing(): is identical to isinstance(x, Thing). You can also match on attributes with attribute= keyword-like syntax and positionally if the class supports it.

Read more in the what’s new page or in PEP 636, which is a tutorial PEP.

Typing improvements

As always, most of these improvements can either be imported from typing_extensions or can be used in 3.7+ with from __future__ import annotations as long as you are not using them at runtime.

Type Unions

The biggest feature for typing is the native support for the union operator. You can now spell Union[int, str] as int | str, and Optional[str] as str | None. This really improves readability and removes many of the imports needed to specify types (especially combined with 3.9’s stdlib generics). As a bonus, this new union is usable at runtime in isinstance in 3.10+.

Other

A new system was developed to allow libraries to add parameters to a callable (think decorators like pytest and nox provide).

A function can now be a type guard - a function that returns a boolean that is tied to the types it sees. So you could write a function like is_int and the typing system will know that the argument is an int if the function returns True.

You can use TypeAlias to specify that a string is really a type alias and not just a string.

Other improvements

Parenthesized context managers

Due to the new PEG parser, you can now officially add parenthesis around context managers. This is now the nicest way to apply several at a time:

with (
    A() as a,
    B(),
    C() as c,
):
    ...

This was technically supported in CPython 3.9 if you were using the new parser (the default). In 3.10, the old parser has been removed.

Strict zip

zip now has a strict flag that you can set; this was hard to do efficiently on arbitrary iterators any other way, and usually strict=True is what you want. For example:

nums = [1, 2, 3]
lets = "ABC"

for num, let in zip(nums, lets, strict=True):
    print(f"{num}: {let}")

If you add or remove to either or the two lists without changing the other, zip will now throw an error rather than just matching the shortest list. Doing this correctly (without iterating over the input twice) was tricky before 3.10.

Minor features

  • int.bit_count() counts the number of ones in the binary representation.
  • __ipow__ now supports __pow__/__rpow__ fallbacks if NotImplemented is returned (like all the other operators).
  • The walrus operator supports unparenthesised usage in a few more places.
  • aiter()/anext() added to parallel iter()/next().
  • staticmethod/classmethod are better at wrapping now.

There were also various changes related to current and future speedups. Functions now cache __globals__['__builtins__'] into .__builtins__, which sounds useful for monkeypatching in tests.

Stdlib improvements

Dataclasses

Dataclasses received a few really huge improvements in 3.10, making them more like the attrs library than ever.

The slots=True option was added, which will generate __slots__ (and recreate the class, if that matters to you).

You can also now set keyword only fields - either on the entire class or a specific field (with kw_only=True), or by defining a special _: KW_ONLY field that will make subsequent fields keyword only. Besides readability and future-proofing against rearranging attribute order, this also allows subclasses to set defaults on attributes in a parent class! In general, mixing subclassing and defaults is much easier with keyword-only support. For example:

@dataclasses.dataclass(kw_only=True)
class Record:
    kind: str
    name: str
    id: int


@dataclasses.dataclass(kw_only=True)
class Movie(Record):
    kind: str = "movie"

Minor features

  • collections.abc.Callable and types.GenericAlias improvements
  • Better async support in contextlib
  • You can specify a root_dir / dir_fd for glob.
  • inspect.get_annotations() added, with a consistent way to get the annotations and unstringize. Also affects inspect.signature().
  • Added itertools.pairwise().
  • Several pathlib improvements related to symlinks, including hardlink_to, which replaces link_to and mimics the API of symlink_to.
  • Several additions to statistics.
  • sys.orig_argv provides the original argv passed in (including the Python exe if present)
  • sys.stdlib_module_names, list of standard library names present.
  • types.EllipsisType/types.NoneType/types.NotImplementedType reintroduced for type checkers.

Deprecations, warnings, and removals

Some of the Python 2 compatibility removals that were delayed from 3.9 were delayed one more release (3.11). There still are a nice set of new deprecations and warnings, though! This includes a lot of the old module loading API. The collections.* aliases to collections.abc.* have been removed.

There’s a new opt-in warning (-X warn_default_encoding) if you forget to specify the encoding when using open. In most cases, you want encoding="utf-8", rather than the default (which is now possible to specify using "locale"). Python 3.15 will change the default to be "utf-8", so you should always specify this when using open in text mode to be forward compatible. And you can now force Python to use utf-8 by default with PYTHONUTF8 envvar or -X utf8 on the command line.

Distutils is officially deprecated, slated for removal in 3.12.

Final words

Python 3.10 is a solid release, with solid improvements to errors and typing, and a fun new feature (pattern matching) to play with. It’s already powering Pyodide, a WebAssembly version of Python for your browser with compiled extension support.

Sources and resources


Posts in the Python Series2→3 3.7 3.8 3.9 3.10 3.11 3.12 3.13