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:
Now in Python 3.11 that's how it looks like:
The characters "~
" and "^
" point out exactly where the issue is located.
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:
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:
blog
as required key and required valuetwitter
as required key (but optional value)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 π