New Features in Python 3.10

A look at the best features included in the latest iteration of Python

Photo by Ben Sweet on Unsplash

The second alpha version of Python 3.10 was released at the beginning of November — and with it, we are able to see a glimpse of what’s next for Python.

Some exciting moves are being made that will likely change the future Python ecosystem towards more explicit, readable code — while maintaining the ease-of-use that we all know and love.

Thanks to the new annual release schedule [PEP 602], we now see shorter development windows — meaning many of the features we can expect in October 2021 are already here in some form or another — we will cover everything new so far:

Further Extensions to Type Annotations
- Postponed Evaluation of Annotations
- Type Union Operator
- TypeAlias AnnotationWhy Typing is ImportantNew Methods and Behaviors
- Equal Length Flag for zip()
- Bit Count for Integers
- Mapping for Dictionary Views

If you prefer video, we cover everything from this article here:

*All article images have been produced by the author, except where stated otherwise.

Further Extensions to Type Annotations

Version 3.9 introduced a big overhaul and cleanup of type hinting and annotations in Python. This looks like a continuing trend with further additions to typing in 3.10.

Timeline of changes to type annotations from Python 3.0 to now.

1. Postponed Evaluation of Annotations

The evaluation of type annotation has always been performed at function definition time, meaning that type annotations are evaluated line-by-line in a top-down fashion.

Although it seems logical, there are two issues to doing this:

  • Type hints that refer to types that have not been defined yet (forward references) do not work — and must be expressed as strings. We would need to write "int" rather than int (although this is only the case for custom, not built-in/pre-defined types).
  • It slows down module imports as type hints are executed at that time.

So instead, annotations will be stored in __annotations__ and the evaluation of these can be performed together — allowing forward referencing and executing module imports first (reducing initialization time).

2. Type Union Operator

3.10 introduces conditional logic with the | operator. When annotating data types, we can use | as OR. For example, we have a variable that is expected to be either an int OR float — we write it as int | float, like this:

def f(x: int | float) -> float:
return x * 3.142f(1) # pass
f(1.5) # pass
f('str') # linter will show annotation error

Pre-3.10, the equivalent operation would be written using the typing.Union method — like Union[int, float].

3. TypeAlias Annotation

Back to the forward referencing issue, a common solution to avoiding forward references is to write them as strings.

However, writing types as strings can cause problems when assigning those types to variables — as Python will assume our string literal type annotation is simply a string.

Using that type annotation variable in places that we would usually use type annotations will return an error. For example:

MyType = "ClassName"  # ClassName is our type annotation
def foo() -> MyType:

Here, we are attempting to use MyType as an alias for a type, however, MyType will be read as a string value — not a type alias.

This is valid as long as ClassName is defined later in the code. Currently, this will throw annotation errors.

To solve this, a method for explicitly identifying MyType as a type alias has been added:

from typing_extensions import TypeAliasMyType: TypeAlias = "ClassName"
def foo() -> MyType:
...ORMyType: TypeAlias = ClassName # if we have defined ClassName already
def foo() -> MyType:

Why Typing is Important

Although this is certainly not a huge, game-changing move, it’s incredibly cool to see the Python developers doubling down on enhancing the typing features.

The strength of Python comes with its ease of use and the lack of a steep learning curve. One of the reasons for this is the lack of need to explicitly define types throughout our code.

It may seem counterintuitive, but giving developers the option to define types can greatly enhance the readability and maintainability of code-bases. For example, take these extracts from the source code of the Transformers library:

Even without context, we can read this code and immediately grasp what data we should expect to be fed into these functions, classes, and methods — and exactly which datatypes we should be expecting to return.

In complex code bases (and even simple ones), type annotation can massively improve readability. Simultaneously, not everyone will want (or need) to use them — so an optional, exception-free functionality strikes a perfect balance.

These changes show a commitment to the type annotation features of Python. The added clarity to our favorite libraries (and our own code) may leave an interesting long-term impact on the Python ecosystem.

New Methods and Behaviors

Alongside the new typing changes, we also have a few updates and new features to other core Python functionalities.

Equal Length Flag for Zip

The first of those comes with PEP 618, which adds an optional strict flag to the zip() function. If we set strict=True an error will be raised if both inputs to zip are not of an equal length.

Without the strict=True flag (left), no error is raised, and the longer list is truncated to create the zipped generator. With strict=True, an error is raised.

Rather than blindly truncating mismatched data, the new strict argument allows us to control the behavior of zip — something that will save plenty of developers from a headache somewhere down the line.

Bit Count for Integers

Also known as a population count, this new method allows us to count the number of ones in the binary representation of an integer. All we write is int.bit_count().

We are simply returning the count of positive bits in the binary values representing each integer:

0   = 00000000
1 = 00000001
2 = 00000010
3 = 00000011
10 = 00001010
11 = 00001011
12 = 00001100
100 = 01100100
101 = 01100101
102 = 01100110

Mapping for Dictionary Views

Three dictionary methods dict.keys(), dict.values(), and dict.items() all return a different view of a dictionary. Now, a mapping attribute has been added to each of these view objects.

This new attribute is a types.MappingProxyType object that wraps around the original dictionary — if we call it on a view, we return the original dictionary.

That’s all for now — although we’re only a few months into the development timeline for 3.10, there are already plenty of interesting changes!

Python’s evolution is continuing and looks like there will be plenty more fascinating features added to the language over time.

I hope you enjoyed this article. If you have any questions, ideas, or suggestions — let me know via Twitter or in the comments below.

Thanks for reading!