Python 3.12: What's New

Last updated October 3rd, 2023

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:

  1. Syntactic formalization of f-strings - more flexible f-strings
  2. A per-interpreter GIL - take full advantage of multiple CPU cores
  3. Better error messages - hints for mistakes like not importing a module from the standard library
  4. Improved typing
    1. Using TypedDict for more precise **kwargs typing
    2. Override decorator for static typing
  5. 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:

  1. PEP-701
  2. Official docs

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.

Per-interpreter GIL

More info:

  1. PEP-684
  2. Official docs
  3. A Per-Interpreter GIL: Concurrency and Parallelism with Subinterpreters

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-

  1. "Did you mean..."
  2. "Did you forget..."
  3. "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:

  1. Python 3.10: Clearer Error Messages
  2. Python 3.11: Better Error Messages

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:

  1. Official docs
  2. PEP-692
  3. PEP-698

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:

  1. PEP-709
  2. Official docs

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!

Jan Giacomelli

Jan Giacomelli

Jan is a software engineer who lives in Ljubljana, Slovenia, Europe. He is a Staff Software Engineer at ren.co where he is leading backend engineering efforts. He loves Python, FastAPI, and Test-Driven Development. When he's not writing code, deploying to AWS, or speaking at a conference, he's probably skiing, windsurfing, or playing guitar. Currently, he's working on his new course Complete Python Testing Guide.

Share this tutorial

Featured Course

Serverless Apps with FastAPI, DynamoDB, and Vue

Build serverless applications with FastAPI and Vue that run on AWS.

Featured Course

Serverless Apps with FastAPI, DynamoDB, and Vue

Build serverless applications with FastAPI and Vue that run on AWS.