All The Important Features and Changes in Python 3.10
Tags: python
Published: 2021-03-30 17:27:08

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:

```shell
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:

```shell
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:

```python
# 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:

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

## Type Aliases Syntax Change

In earlier [versions of Python](https://www.python.org/dev/peps/pep-0484), 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:

```python
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:

```python
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](https://www.python.org/dev/peps/pep-0612). 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)_:

```python
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:

```python
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:

```python
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)):

```shell
~ $ ./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-41718](https://bugs.python.org/issue41718), [bpo-42927](https://bugs.python.org/issue42927) and [bpo-43452](https://bugs.python.org/issue43452).

## 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:  

```python
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 `case`s 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:

```python
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:

```python
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 `case`s 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:

```python
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](https://www.python.org/dev/peps/pep-0636/).

## 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](https://docs.python.org/3.10/whatsnew/3.10.html) 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.