Python 3.11 What's New?

Here is a selection of the major changes coming in Python 3.11:

1️⃣ Better Error Handling

Better error messages to easily spot issues in your code.

Consider some code trying to read an invalid key from your dict.

In this case, instagram is an unexistent key that process_dict attempts to read:

invalid_dict = dict(
    youtube="Gui Commits",
    blog="https://guicommits.com",
    twitter="guilatrova",
)

def process_dict(input_dict: dict):
    handles = ["youtube", "blog", "twitter", "instagram"]
    social_links = []
    for handle in handles:
        social_links += input_dict[handle]

process_dict(invalid_dict)

If you were running it in Python 3.10, you would see this:

Python 3.10 Displaying an error

Now in Python 3.11 that's how it looks like:

The characters "~" and "^" point out exactly where the issue is located.

Python 3.11 Displaying an error

2️⃣ Exception Groups

Now we have an ExceptionGroup that groups many other exceptions inside.

Each exception living inside a group can be individually captured by the except* syntax. See:

def raise_exception_group():
    raise ExceptionGroup(
        "Description from ExceptionGroup", # Group wrapper
        [
            ValueError("ValueError"), # First exception in group
            TypeError("TypeError") # Second exception in group
        ]
    )


def main():
    try:
        raise_exception_group()
    except* ValueError:
        print("Value Error!")
    except* TypeError:
        print("Type Error!")


main()

Note the code above would print both Value Error! and Type Error!. All except blocks are executed if any exception exists inside the ExceptionGroup.

It can get a bit crazy since you have groups inside groups!

def raise_exception_group():
    raise ExceptionGroup(
        "Description from ExceptionGroup",
        [
            ValueError("ValueError"),
            TypeError("TypeError"),
            ExceptionGroup("Another group", # Second group inside main group
                [
                    Exception("Another error")
                ]
            )
        ]
    )

3️⃣ Exception add_note

Now you can keep enriching your exceptions with further data to add even more context to them!

def raise_exception():
    raise Exception("How you aren't subscribed to this blog's newsletter? 🀯😱")


def main():
    try:
        raise_exception()
    except Exception as ex:
        ex.add_note("Not following on Twitter would also be a mistake! 😜") # πŸ‘ˆ New method!
        raise


main()

So your traceback would display all notes added:

Python 3.11 exception's add_note feature giving you some good advice

4️⃣ New Type Hints

Self Type

The self type came to resolve a common issue where Python/IDE can't infer the self type.

Consider this working code from previous Python 3.10:

from __future__ import annotations

class Shape:
    def set_scale(self, scale: float) -> Self:
        self.scale = scale
        return self

class Circle(Shape):
    def set_radius(self, r: float) -> Self:
        self.radius = r
        return self

# πŸ”΄ Invalid inferred type!
Circle().set_scale(0.5)
Circle().set_scale(0.5).set_radius(2.7)

My IDE keeps telling me that Circle.set_scale returns Shape, which is not true!

This gets resolved with the new Self type:

from typing import Self  # πŸ‘ˆ New typing

class Shape:
    def set_scale(self, scale: float) -> Self:
        self.scale = scale
        return self

class Circle(Shape):
    def set_radius(self, r: float) -> Self:
        self.radius = r
        return self

# 🟒 Correct inferred type!
Circle().set_scale(0.5)
Circle().set_scale(0.5).set_radius(2.7)

LiteralString

Think LiteralString as specific strings typed and expected by the developer.

from typing import LiteralString # πŸ‘ˆ New import

def inferred(s: str) -> bool:
    if s == "Did you already signed up for our newsletter?":
        print(s)  # πŸ‘ˆ Python identifies this as Literal String
        return True
    else:
        print(s)  # πŸ‘ˆ This is still any string
        return False


def defined(s: LiteralString) -> bool:
    print(s)
    return True


defined("literal value")

It can be useful to spot and prevent SQL Injection issues.

NotRequired for TypedDict

This feature is already common in TypeScript.

Consider you can define one key's dictionary with either:

  • One value as potentially null (already supported)
  • One key as potentially missing (new in Python 3.11)

See a few examples of how it would work for a dict with three keys:

  1. blog as required key and required value
  2. twitter as required key (but optional value)
  3. instagram as optional key (but required value)
from typing import NotRequired, TypedDict

class MediaLinks(TypedDict):
    blog: str
    twitter: str | None # πŸ‘ˆ Key is required, value can be either null or str
    instagram: NotRequired[str] # πŸ‘ˆ Key is not required, but if set it should be str


# 🟒 Valid
links_with_instagram: MediaLinks = {"blog": "Gui Commits", "twitter": "@guilatrova", "instagram": "non existent"}
links_without_instagram: MediaLinks = {"blog": "Gui Commits", "twitter": None}


# πŸ”΄ Invalid
links1: MediaLinks = {"blog": "Gui Commits", "twitter": "@guilatrova", "instagram": None}
# 1) 'instagram' can't be None. Mypy output:
# Incompatible types (expression has type "None", TypedDict item "instagram" has type "str")

links2: MediaLinks = {"blog": "Gui Commits"}
# 2) 'twitter' is expected. Mypy output:
# Missing key "twitter" for TypedDict "MediaLinks"

links3: MediaLinks = {"blog": "Gui Commits", "twitter": "@guilatrova", "youtube": "Someday?"}
# 3) 'youtube' isn't expected. Mypy output:
# Extra key "youtube" for TypedDict "MediaLinks"

Remember that Python validates nothing for you.

You must use Mypy to guarantee your typings are correct.

πŸ’‘ How to use new types before Python 3.11 is released?

Did you know you can start using NotRequired, Self, and LiteralString types already?

You just have to install typing_extensions:

πŸ‘‰ pip install typing_extensions

πŸ’β€β™‚οΈ Impot types from typing_extensions and that's it.

5️⃣ Performance Improvement

Python 3.11 is on average 25% faster than 3.10.

Due to improvements on CPython providing faster startup and faster runtime.

6️⃣ New default module: tomllib

Now Python supports reading toml by default:

import tomllib

toml_str = """
[build]
python-version = "3.11.0"
python-implementation = "CPython"

[tool.poetry.dev-dependencies]
tryceratops = "^1.1.0"
"""

data = tomllib.loads(toml_str)
print(data)

Toml is very common in pyproject.toml files. See this real file from the Tryceratops project.

7️⃣ StrEnum

New type StrEnum with auto() support so you don't have to type

from enum import StrEnum, auto

class Status(StrEnum):
    ON_HOLD = auto()
    IN_PROGRESS = auto()
    ON_REVIEW = auto()
    DONE = auto()


print(Status.ON_HOLD) # Output: on_hold
print(Status.ON_HOLD == "on_hold") # Output: True

Now that you know about Python 3.11 changes

If you enjoyed this article did you know you can receive such updates in a shorter/funnier way?

Check out this πŸ‘‡ thread and consider following @guilatrova πŸ˜‰