All The Important Features and Changes in Python 3.10

The release of Python 3.10 is getting closer, so it’s time to take a look at most important new features and changes it’s going to bring

It’s that time of year again when last Python alpha release rolls around and first beta version is on it’s way, so it’s ideal time to take new version of Python for a ride and see what cool new features are incoming – this time around – in Python 3.10!

Installing Alpha/Beta Version

If you want to try out all the features of the latest and greatest version of Python, then you will need to install the Alpha/Beta version. However, considering that this is not yet a stable version, we don’t want to overwrite our default Python installation with it. So, to install Python 3.10 alongside our current interpreter, we can use the following:

wget https://www.python.org/ftp/python/3.10.0/Python-3.10.0a6.tgz
tar xzvf Python-3.10.0a6.tgz
cd Python-3.10.0a6
./configure --prefix=$HOME/python-3.10.0a6 make make install$HOME/python-3.10.0a6/bin/python3.10

After running the above code, you will be greeted by the Python 3.10 Alpha IDLE:

Python 3.10.0a6 (default, Mar 27 2021, 11:50:33) [GCC 9.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>>

With Python 3.10 installed, we can take a look at all the new features and changes…

Type Checking Improvements

If you use type checking in Python you will be happy to hear that Python 3.10 will include a lot of type checking improvements, including Type Union Operator with cleaner syntax:

# Function that accepts either int or float
# Old:
def func(value: Union[int, float]) -> Union[int, float]:
return value

# New:
def func(value: int | float) -> int | float:
return value

On top that, this simple improvement is not limited just to type annotations, but can be also used with isinstance() and issubclass() functions:

isinstance("hello", int | str)
# True

Type Aliases Syntax Change

In earlier versions of Python, type aliases were added to allow us to create aliases that represent user-defined types. In Python 3.9 or earlier, this would be done like so:

FileName = str

def parse(file: FileName) -> None:
...

Here FileName is an alias for basic Python string type. Starting with Python 3.10 though, the syntax for defining type aliases will change to the following:

FileName: TypeAlias = str

def parse(file: FileName) -> None:
...

This simple change will make it easier both for programmers and type checkers to distinguish between ordinary variable assignment and type alias. This change is also backward compatible, so you don’t have to update any of your existing code that uses type aliases.

Apart from these two changes, there are also other improvements to typing module – namely Parameter Specification Variables in PEP 612. These however, aren’t really something you would find in most Python codebases as they are used for forwarding parameter types of one callable to another callable (for example in decorators). In case you have use case for such a thing though, go check out the above mentioned PEP.

Population Count

Starting with Python 3.10 you can use int.bit_count() to calculate bit count (number of one’s) in binary representation of a integer. This is also known as Population Count (popcount):

value = 42
print(bin(value))
# '0b101010'
print(value.bit_count())
# 3

This is definitely nice, but let’s be real, implementing this function isn’t exactly difficult, it’s really just one line of code:

def bit_count(value):
return bin(value).count("1")

With that said, it’s another convenient function which might come in handy at some point and these kinds of useful little features are one of the the reasons why Python is so popular – seemingly everything is available out of the box.

distutils Are Being Deprecated

With the new version things aren’t being only added, but also deprecated/removed. That’s the case for distutils package, which is deprecated in 3.10 and will be removed in 3.12. This package has been replaced by setuptools and packaging for a while now, so if you’re using either of these, then you should be fine. With that said, you should probably check your code for usages of distutils and start preparing to get rid of it sometime soon.

Context Manager Syntax

Python context managers are great for opening/closing files, handling database connections and many other things, and in Python 3.10 their syntax will receive a little quality of life improvement. This change allows for parenthesized context managers to span multiple lines, which is handy if you want to create many of them in single with statement:

with (
open("somefile.txt") as some_file,
open("otherfile.txt") as other_file,
):
...

from contextlib import redirect_stdout

with (open("somefile.txt", "w") as some_file,
redirect_stdout(some_file)):
...

And as you can see from the above, we can even reference variable created by one context manager (... as some_file) in another one following it!

These are just 2 of the many new formats available in Python 3.10. This improved syntax is quite flexible, so I won’t bother showing every possible formatting option as I’m pretty sure that whatever you will throw at Python 3.10, it will most likely just work.

Performance Improvements

As has been the case with all the recent releases of Python, Python 3.10 also brings some performance improvements. First of them being optimization of str()bytes() and bytearray() constructors, which should be around 30% faster (snippet adapted from Python bug tracker [example](https://bugs.python.org/issue41334)):

~ $./python3.10 -m pyperf timeit -q --compare-to=python "str()" Mean +- std dev: [python] 81.9 ns +- 4.5 ns -> [python3.10] 60.0 ns +- 1.9 ns: 1.36x faster (-27%) ~$ ./python3.10 -m pyperf timeit -q --compare-to=python "bytes()"
Mean +- std dev: [python] 85.1 ns +- 2.2 ns -> [python3.10] 60.2 ns +- 2.3 ns: 1.41x faster (-29%)
~ \$ ./python3.10 -m pyperf timeit -q --compare-to=python "bytearray()"
Mean +- std dev: [python] 93.5 ns +- 2.1 ns -> [python3.10] 73.1 ns +- 1.8 ns: 1.28x faster (-22%)

Another more noticeable optimization (if you’re using type annotations) is that function parameters and their annotations are no longer computed at runtime, but rather at compilation time. This now makes it around 2 times faster to create a function with parameter annotations.

On top of that, there are some more optimizations in various parts of Python core. You can find specifics about those in following issues in Python bug tracker: bpo-41718bpo-42927 and bpo-43452.

Pattern Matching

The one big feature you surely already heard about is Structural Pattern Matching. This will add case statement that we all know from other programming languages. We all know how to use case statement, but considering that this is Python – it’s not just plain switch/case syntax, but it also adds some powerful features along with it that we should explore.

Pattern matching in it’s most basic form consists of match keyword followed by expression, whose result is then tested against patterns in successive case statements:

def func(day):
match day:
case "Monday":
return "Here we go again..."
case "Friday":
return "Happy Friday!"
case "Saturday" | "Sunday":  # Multiple literals can be combined with |
return "Yay, weekend!"
case _:
return "Just another day..."

In this simple example, we use day variable as our expression which is then compared with individual strings in case statements. Apart from the cases with string literals, you will also notice the last case which uses _wildcard, which is equivalent to default keyword present in other languages. This wildcard case can be omitted though, in which case no-op may occur, which essentially means that None is returned.

Another thing to notice in the code above, is the usage of | which makes it possible to combine multiple literals using | (or) operator.

As I mentioned, this new pattern matching doesn’t end with the basic syntax, but rather brings some extra features, such as matching of complex patterns:

def func(person):  # person = (name, age, gender)
match person:
case (name, _, "male"):
print(f"{name} is man.")
case (name, _, "female"):
print(f"{name} is woman.")
case (name, age, gender):
print(f"{name} is {age} old.")

func(("John", 25, "male"))
# John is man.

In the above snippet we used tuple as the expression to match against. We’re however not limited to using tuples – any iterable will work here. Also, as you can see above, the _ wildcard can be also used inside the complex patterns and not just by itself as in the previous example.

Using plain tuples or lists might not always is the best approach, so if you prefer to use classes instead, then this can be rewritten in the following way:

from dataclasses import dataclass

@dataclass
class Person:
name: str
age: int
gender: str

def func(person):  # person is instance of Person class
match person:
# This is not a constructor
case Person(name, age, gender) if age < 18:  # guard for extra filtering
print(f"{name} is a child.")
case Person(name=name, age=_, gender="male"):  # Wildcard ("throwaway" variable) can be used
print(f"{name} is man.")
case Person(name=name, age=_, gender="female"):
print(f"{name} is woman.")
case Person(name, age, gender):  # Positional arguments work
print(f"{name} is {age} years old.")

func(Person("Lucy", 30, "female"))
# Lucy is woman.
func(Person("Ben", 15, "male"))
# Ben is a child.

Here we can see that it’s possible to match against class’s attributes with patterns that resemble class constructor. When using this approach, also individual attributes get captured into variables (same as with tuples shown earlier), which we can then use in respective case‘s body.

Above we can also see some other features of pattern matching – in first case statement it’s a guard, which is a if conditional that follows the pattern. This can be useful if matching by value is not enough and you need to add some additional conditional check. Looking at the remaining cases here, we can also see that both keyword (e.g. name=name) and positional arguments work with this constructor-like syntax, and same also goes for _ (wildcard or “throwaway”) variable.

Pattern matching also allows for usage of nested patterns. These nested patterns can use any iterable, both with constructor-like objects or more iterables inside of them:

match users:
case [Person(...)]:
print("One user provided...")
case [Person(...), Person(...) as second]:  # as var can be used to capture subpattern
print(f"There's another user: {second}")
case [Person(...), Person(...), *rest]:  # *var can be used as unpacking
print(...)

In these kinds of complex patterns it might be useful to capture subpattern into variable for further processing. This can be done using as keyword, as shown in the second case above.

Finally, * operator can be used to “unpack” variables in the pattern, this also works with _ wildcard using the *_ pattern.

If you want to see more examples and complete tutorial, then check out PEP 636.

Closing Thoughts

Python 3.10 brings many interesting new features, but this being alpha (and soon to be beta) release, it’s still far from fully tested and production ready. Therefore it’s definitely not a good idea to start using it just yet. So, it’s probably best to sit back and wait for full release in October and maybe check What’s New In Python 3.10 page from time to time for any last minute additions.

With that said – if you’re eager to upgrade – it might not be a bad idea to grab the first beta release (coming sometime in June) and take it for a test run to see if your existing codebase is compatible with all the incoming changes, deprecations or removals of functions/modules.