Another year gone, another Python version is out! Python 3.12 was released on October 2nd, 2023. This article looks at the most interesting new additions to the Python language that will help to make your Python code even cleaner:
- Syntactic formalization of f-strings - more flexible f-strings
- A per-interpreter GIL - take full advantage of multiple CPU cores
- Better error messages - hints for mistakes like not importing a module from the standard library
- Improved typing
- Using
TypedDict
for more precise**kwargs
typing - Override decorator for static typing
- Using
- Comprehension inlining - faster list/dict/set comprehensions
Contents
Installing Python 3.12
If you have Docker, you can quickly spin up a Python 3.12 shell to play around with the examples in this article like so:
$ docker run -it --rm python:3.12
Not using Docker? We recommend installing Python 3.12 with pyenv:
$ pyenv install 3.12.0
You can learn more about managing Python with pyenv from the Modern Python Environments - dependency and workspace management article.
Syntactic Formalization of F-Strings
How many times have you been furious about having to switch between single and double quotes when using f-strings? You know what I'm talking about, right?
friends = ["Rolf", "Bob", "Jen", "Anne"]
print(f"I invited: {", ".join(friends)}")
"""
print(f"I invited: {", ".join(friends)}")
^
SyntaxError: f-string: expecting '}'
"""
So, you'd need to update your code like so:
friends = ["Rolf", "Bob", "Jen", "Anne"]
print(f'I invited: {", ".join(friends)}')
# I invited: Rolf, Bob, Jen, Anne
Well, this is no longer the case. Python 3.12 introduces syntactic formalization of f-strings, which means that you can use the same type of quotes used in the beginning/end inside of the f-strings themselves. This seems like a small change, but I'm sure you'll love it. This code works with Python 3.12:
friends = ["Rolf", "Bob", "Jen", "Anne"]
print(f"I invited: {", ".join(friends)}")
# I invited: Rolf, Bob, Jen, Anne
Besides that, you can now also use comments and multiline expressions inside f-strings:
users = [{"id": 1, "name": "John"}, {"id": 2, "name": "Jane"}]
print(
f"User IDs: {", ".join([str(user["id"]) for user in users])
# We need to convert IDs to string because join expects string.
}"
)
While this is cool, be careful to not introduce too much complexity into your f-strings. It's easy to go overboard and make them unreadable.
What's more, with these features, it's even more important to use a linter to keep your code clean and readable. For example, such code is now valid Python code:
print(
f"User IDs: {
", "
.join(
[
str(
user[
"id"
# I like it when it's a mess
]
)
for user # Very important line
in users
]
)
z# We need to convert IDs to string because join expects string.
}"
)
While it might be syntactically correct, it's far from being readable. Therefore, one more time -- use a linter!
If you want to learn more about linters and Python code-quality in general, check out the Python Code Quality tutorial
More info:
A Per-Interpreter GIL
The Global Interpreter Lock (GIL) is a mutex (or lock) that protects access to Python objects, preventing multiple threads from executing Python bytecodes at once. This lock is necessary mainly because CPython's memory management is not thread-safe. The GIL is controversial because it prevents multithreaded CPython programs from taking full advantage of multiprocessor systems in certain situations.
The good news is that, as part of PEP-684, Python 3.12 introduces a per-interpreter GIL. This change will allow Python to take full advantage of multiple CPU cores. Unfortunately, there is some bad news: Python 3.12 will only support the per-interpreter GIL through the C-API. Full Python-API support will be added in Python 3.13.
More info:
Better Error Messages
Like the last two versions of Python, Python 3.12 also brings better error messages. This time, it's about hints for resolving the errors with suggestions like-
- "Did you mean..."
- "Did you forget..."
- "Did you mean to use..."
Let's take a look at some examples...
Standard library modules are now potentially suggested as part of the error messages displayed by the interpreter when a NameError
is raised to the top level:
if os.getenv("DEBUG"):
print("Debugging is on")
"""
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
NameError: name 'os' is not defined. Did you forget to import 'os'?
"""
Improved error suggestions for NameError
exceptions for instances:
class User:
def __init__(self, first_name, last_name):
self.first_name = first_name
self.last_name = last_name
def full_name(self):
return f"{self.first_name} {last_name}"
user = User("Jan", "Smith")
print(user.full_name())
# NameError: name 'last_name' is not defined. Did you mean: 'self.last_name'?
Improved SyntaxError
error message when the user types import x from y instead of from y import x:
import datetime from datetime
# SyntaxError: Did you mean to use 'from ... import ...' instead?
ImportError
exceptions raised from failed from <module> import <name>
statements now include suggestions for the value of <name>
based on the available names in <module>
:
from datetime import TimeDelta
# ImportError: cannot import name 'TimeDelta' from 'datetime'. Did you mean: 'timedelta'?
I'm sure you'll appreciate these improvements. Anything that can help solve the errors faster is a good thing.
Review the official docs for more info.
Also, check out the following resources if you're curious about the error message improvements to the previous two versions of Python:
Improved Typing
Python 3.12 also brings some improvements to typing. Let's take at look at the most interesting improvements.
The first one is the ability to better type kwargs
with TypedDict
. Have you ever tried to type kwargs
that were of different types? I bet you ended up with Any
or some other sub-optimal solution. Function signatures, as introduced by PEP-484, allowed type annotations for kwargs
only if they were of the same type. This could be very annoying, when you try to type a bit more evolved codebase. Fortunately, Python 3.12 comes with support to type kwargs
with different types by using TypedDict
:
from typing import TypedDict, Unpack
class OAuthState(TypedDict):
name: str
user_id: int
is_internal: bool
def redirect_to_idp(**kwargs: Unpack[OAuthState]):
...
Another improvement worth mentioning is the @override
decorator. Have you ever tried to override some method to extend behavior of a class and end up scratching your head when things don't work as expected? This sure has happened to me. For example, I wanted to extend the behavior of a Django REST Framework view, but something was off. It took me quite some time to realize that my method that should override put
was called putt
. Python 3.12 introduces protection for that via the @override
decorator. If a method is decorated with @override
, Python type checkers will check if the method is actually overriding something. If not, it will raise an error. This is how it works:
from typing import override
class Cat:
def meow(self) -> str:
return "meow"
class EgyptianCat(Cat):
@override # ok: overrides Cat.meow
def meow(self) -> str:
return "meow meow"
class PersianCat(Cat):
@override # type checker error: does not override Cat.mew
def mew(self) -> str:
return "meow mew"
# mypy error
# error: Method "mew" is marked as an override, but no base method was found with this name [misc]
More info:
Comprehension Inlining
Python 3.12 also brings a new optimization for list/dict/set comprehensions. Comprehensions are now inlined, which means that they are faster. Instead of creating a new single-use function for each execution, comprehensions are now inlined. This speeds up comprehensions up to 2x.
More info:
Conclusion
Although it may not seem like a huge release, it comes with improvements that will make our lives easier. I'm sure you'll appreciate the syntactic formalization of f-strings, better error messages, and improved typing. I'm also sure that you'll be happy to see that Python is finally taking full advantage of multiple CPU cores. I'm sure you're already looking forward to Python 3.13, which will bring Python-API support for per-interpreter GIL. Until then, happy coding!