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-41718, bpo-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 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:
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 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:
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.