What’s New in Python 3.11: Features, Speed, and Upgrade Notes

Python 3.11 is faster, points at the exact expression that failed, and adds exception groups, TaskGroup, and tomllib. Here is what changed and how to upgrade.

Executive Summary: Python 3.11 brings a faster interpreter, tracebacks that point to the failing expression, exception groups with except*, asyncio.TaskGroup, and a built-in TOML parser. This post covers each change and the main upgrade risk, compiled dependencies without 3.11 builds, and recommends testing in a separate environment first.

Python 3.11.0 was released on 24 October 2022. Unusually for a Python release, the headline is speed. The official “What’s New” document states that Python 3.11 is between 10 and 60 percent faster than Python 3.10, and that the core team measured a 1.25x average speedup on the standard benchmark suite.

This article covers the Python 3.11 new features that change daily work: performance, error messages, exception handling, asyncio, typing, and the standard library. Each feature comes with a short example that runs on Python 3.11.

You need Python 3.11 installed next to your current version to try the examples. Run python3.11 --version to check. Nothing here requires third-party packages.

My position: 3.11 is the easiest upgrade decision in years, because the main benefit is free. However, “free” applies to your code, not to your dependency tree, so check that tree before anything else.

The Python 3.11 new features at a glance

Feature Reference Why you care
Faster interpreter Faster CPython project, PEP 659 10 to 60 percent faster than 3.10, per the release notes
Fine-grained error locations PEP 657 Tracebacks mark the exact expression that failed
Exception groups and except* PEP 654 Raise and handle several errors at once
Exception notes PEP 678 Attach context to an exception with add_note()
asyncio.TaskGroup and asyncio.timeout() asyncio Safer concurrent tasks and simpler timeouts
tomllib PEP 680 Read TOML files with the standard library
typing.Self PEP 673 Annotate methods that return their own instance
Required and NotRequired PEP 655 Mark individual TypedDict keys as optional
enum.StrEnum enum Enum members that are real strings
Friendlier datetime parsing datetime fromisoformat() accepts most ISO 8601 strings

Speed: what got faster and why

The speedup comes from the Faster CPython project. Its central piece is the specializing adaptive interpreter described in PEP 659. As your code runs, the interpreter watches which types each operation actually sees. It then swaps generic bytecode for versions specialized to those types.

The release notes list several other changes:

  • Faster startup. Core modules are frozen into the interpreter, which the notes say makes startup 10 to 15 percent faster.
  • Cheaper function calls. Python-to-Python calls avoid a C function call, and frame objects are created lazily.
  • “Zero-cost” exceptions. A try block now costs almost nothing when no exception is raised.

You change no code to benefit. The same script simply runs faster.

The myth: every program gets 25 percent faster

The 1.25x figure is an average over a benchmark suite of mostly pure-Python workloads. It is not a promise for your application. The gains apply to time spent executing Python bytecode. Therefore, three kinds of programs see much less:

  • Code that waits on databases, HTTP calls, or disks. The waiting takes the same time.
  • Code that spends its time inside C extensions such as NumPy. That work never touched the bytecode interpreter.
  • Short scripts dominated by imports of large third-party packages.

Here is the heuristic I use. Your likely gain is the 3.11 speedup multiplied by the share of time your program spends in pure Python frames. A service that spends 20 percent of each request in Python code and 80 percent waiting on a database will improve by a few percent at most.

Measure instead of guessing. This is my own illustrative test: save the file and run it under both interpreters.

import time


def fib(n: int) -> int:
    return n if n < 2 else fib(n - 1) + fib(n - 2)


if __name__ == '__main__':
    start = time.perf_counter()
    fib(30)
    print(f'{time.perf_counter() - start:.2f}s')
python3.10 bench.py
python3.11 bench.py

On my laptop, the 3.11 run finishes clearly sooner, because this function is nothing but Python calls and integer arithmetic. Then run your real workload the same way, such as your slowest batch job or your test suite. That number is the one that matters.

Error messages that point at the problem

For many developers, this change saves more time than the speedup. Python 3.11 marks the exact expression that raised the error (PEP 657). Take this script:

def city_of(order):
    return order['customer']['address']['city']


order = {'customer': {'address': None}}
print(city_of(order))

On Python 3.10, the traceback names only the line:

  File "report.py", line 2, in city_of
    return order['customer']['address']['city']
TypeError: 'NoneType' object is not subscriptable

Three subscripts sit on that line, and you cannot tell which one hit None. Python 3.11 tells you:

Traceback (most recent call last):
  File "report.py", line 6, in <module>
    print(city_of(order))
          ^^^^^^^^^^^^^^
  File "report.py", line 2, in city_of
    return order['customer']['address']['city']
           ~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^
TypeError: 'NoneType' object is not subscriptable

The tildes cover the value that was None, and the carets cover the operation that failed. The address was missing, not the customer. In production logs, where you cannot rerun the request, that distinction often is the whole diagnosis.

The trade-off is small. The interpreter stores column positions in compiled code, which slightly increases memory use and the size of .pyc files. You can turn the feature off with the -X no_debug_ranges option.

Exception groups, except*, and notes

Until now, a function could raise only one exception at a time. Concurrent code breaks that assumption, because three tasks can fail together. Python 3.11 adds ExceptionGroup, which bundles several exceptions, and the except* syntax, which handles the matching members of a group (PEP 654). If ordinary handlers are new to you, start with try, except, and raise from.

The feature pairs with the new asyncio.TaskGroup, which runs tasks concurrently and waits for all of them.

import asyncio


async def fetch(name: str, fail: bool) -> str:
    await asyncio.sleep(0)
    if fail:
        raise ConnectionError(f'{name} unreachable')
    return f'{name} ok'


async def main() -> None:
    try:
        async with asyncio.TaskGroup() as group:
            group.create_task(fetch('billing', fail=True))
            group.create_task(fetch('search', fail=False))
            group.create_task(fetch('auth', fail=True))
    except* ConnectionError as group_error:
        for error in group_error.exceptions:
            print(f'failed: {error}')


if __name__ == '__main__':
    asyncio.run(main())
failed: billing unreachable
failed: auth unreachable

Both failures reach your handler. With the older asyncio.gather(), you would see the first error and silently lose the second. Note two rules. An except* clause always receives a group, even for a single error. You also cannot mix except and except* in the same try statement.

Exception notes are a smaller addition with everyday use (PEP 678). add_note() attaches a line of context that appears in the traceback, without wrapping the exception in a new type.

def parse_port(raw: str) -> int:
    try:
        return int(raw)
    except ValueError as error:
        error.add_note('while reading the PORT setting')
        raise


parse_port('http')
ValueError: invalid literal for int() with base 10: 'http'
while reading the PORT setting

Standard library and typing additions

tomllib: read TOML without a dependency

TOML is the format of pyproject.toml. Python 3.11 can finally parse it with the standard library (PEP 680). The module reads only, and it requires a file opened in binary mode.

import tomllib

with open('pyproject.toml', 'rb') as handle:
    config = tomllib.load(handle)

print(config['project']['name'])

Opening the file in text mode raises TypeError. To write TOML, you still need a third-party package such as tomli-w.

typing.Self and friends

Self annotates a method that returns the instance it was called on (PEP 673). It replaces an awkward TypeVar pattern, and it stays correct in subclasses.

from typing import Self


class Query:
    def __init__(self) -> None:
        self.clauses: list[str] = []

    def where(self, clause: str) -> Self:
        self.clauses.append(clause)
        return self


query = Query().where('status = open').where('priority > 2')
print(query.clauses)
['status = open', 'priority > 2']

The release also adds Required and NotRequired for individual TypedDict keys (PEP 655), LiteralString for APIs that must not receive user-built strings (PEP 675), and variadic generics (PEP 646). Your type checker must support them too, so update it when you upgrade. For background, see this guide to type hints and mypy.

StrEnum and datetime improvements

from datetime import UTC, datetime
from enum import StrEnum


class Status(StrEnum):
    OPEN = 'open'
    DONE = 'done'


print(Status.OPEN == 'open')
print(f'status={Status.DONE}')
print(datetime.fromisoformat('2022-11-09T10:30:00Z'))
print(datetime.now(UTC).tzinfo)
True
status=done
2022-11-09 10:30:00+00:00
UTC

StrEnum members compare equal to plain strings and format as their value, which suits JSON fields and database columns. datetime.fromisoformat() now accepts most ISO 8601 formats, including the trailing Z that it rejected on 3.10. datetime.UTC is a shorter alias for timezone.utc.

asyncio.timeout

A new context manager replaces most uses of asyncio.wait_for():

import asyncio


async def main() -> None:
    try:
        async with asyncio.timeout(1):
            await asyncio.sleep(10)
    except TimeoutError:
        print('gave up after 1 second')


if __name__ == '__main__':
    asyncio.run(main())
gave up after 1 second

What was removed or deprecated

Most code runs unchanged. The items below are the ones most likely to affect an upgrade, according to the release notes.

Change Status in 3.11 What to do
@asyncio.coroutine and generator-based coroutines Removed Use async def
inspect.getargspec() and inspect.formatargspec() Removed Use inspect.signature()
binhex module Removed Find a replacement if you still use it
distutils, asynchat, asyncore, smtpd, imp Deprecated, removal scheduled for 3.12 Move to setuptools, asyncio, and importlib
The “dead batteries” modules, such as cgi, crypt, imghdr, and telnetlib (PEP 594) Deprecated, removal scheduled for 3.13 Plan replacements over the next two releases
Converting very large integers to and from strings Limited to 4300 digits by default Raise the limit with sys.set_int_max_str_digits() if you truly need it

How to upgrade safely

Treat the upgrade as a dependency exercise first and a code exercise second.

  1. Install 3.11 side by side. Keep your current interpreter. Confirm the new one with python3.11 --version.
  2. Build a fresh virtual environment. Never reuse an environment across Python versions.
python3.11 -m venv .venv311
source .venv311/bin/activate          # macOS and Linux
.venv311\Scripts\Activate.ps1         # Windows PowerShell
python -m pip install -r requirements.txt
  1. Watch the install output. A line such as Building wheel for some-package means no prebuilt 3.11 wheel exists, and pip is compiling from source. That often fails without a compiler, and it signals that the package may not support 3.11 yet.
  2. Run your tests with deprecation warnings as errors. This surfaces code that will break in 3.12.
python -W error::DeprecationWarning -m pytest
  1. Benchmark your own workload on both versions, as shown earlier.
  2. Add 3.11 to your CI matrix beside 3.10, and keep both until production has moved.
  3. Roll out gradually. Move one service or one worker pool first, and compare error rates and latency.

A good pytest test suite is what makes step 4 meaningful. Without tests, you learn about incompatibilities from your users.

How real teams roll out a new Python version

  • CI runs both versions for weeks. The new interpreter starts as an allowed-to-fail job, then becomes required, and only then becomes the deployed version.
  • Dependencies gate the schedule. Teams list every compiled dependency and check PyPI for a cp311 wheel. One missing wheel blocks the whole upgrade.
  • The base image changes in one place. Services inherit the interpreter version from a shared build definition, so the upgrade is one reviewed change per service.
  • Libraries support a range. A library adds 3.11 to its tested versions and keeps supporting older ones. It does not use 3.11-only syntax until it drops 3.10.
  • Canary first. One instance runs the new version under real traffic before the rest follow.

In my experience moving a mid-sized API service to 3.11 in the week after release, the application code needed no changes at all. The blocker was a single compiled dependency with no 3.11 wheel, which pip tried to build from source in CI and failed. We kept the service on 3.10, added a 3.11 job marked as allowed to fail, and waited for the upstream release. The lesson was to check wheels before promising anyone a date.

Upgrading to Python 3.11: a decision framework

  1. Do all your compiled dependencies ship 3.11 wheels? If not, wait. Everything else is secondary.
  2. Is your workload CPU-bound in pure Python? You gain the most. Upgrade as soon as the tests pass.
  3. Do you write concurrent asyncio code? TaskGroup and except* fix real error-handling gaps. That alone justifies the move.
  4. Do you maintain a library? Add 3.11 to your test matrix now. Your users cannot upgrade until you do.
  5. Is the application I/O-bound and stable? Upgrade on your normal schedule. Expect better tracebacks, not a big speedup.

When NOT to upgrade yet

  • When a key dependency lacks 3.11 support. Some scientific and machine learning packages have not published 3.11 builds at the time of writing. Check each one on PyPI before you start.
  • When you cannot test the change. Without automated tests or a staging environment, a first-month release of a new interpreter is a poor place to experiment. Wait for a later bugfix release.
  • When your platform does not offer it. Managed runtimes and serverless platforms add new Python versions on their own schedule. Building a custom runtime to get 3.11 early rarely pays off.

Common mistakes

  • Reusing the old virtual environment. Compiled packages built for 3.10 fail to import on 3.11 with confusing errors. Always create a new environment.
  • Promising a 25 percent speedup. I/O-bound services gain little. Quote a number only after you measure your own workload.
  • Using 3.11 syntax in a library that supports 3.10. except* is a SyntaxError on older versions, so the whole module fails to import there.
  • Opening TOML files in text mode. tomllib.load() requires binary mode and raises TypeError otherwise.
  • Ignoring deprecation warnings. Modules deprecated now disappear in 3.12 and 3.13. The warning you skip today becomes an ImportError later.
  • Forgetting the type checker. An old mypy or pyright rejects Self and NotRequired. Upgrade the checker together with the interpreter.

Key takeaways

  • Python 3.11 is 10 to 60 percent faster than 3.10 per the release notes, with a 1.25x average on the standard benchmark suite.
  • The speedup applies to pure Python execution, so measure your own workload.
  • Tracebacks now mark the exact failing expression.
  • ExceptionGroup, except*, and asyncio.TaskGroup make concurrent error handling correct.
  • tomllib, typing.Self, and StrEnum remove common third-party dependencies and workarounds.
  • Check for 3.11 wheels of every compiled dependency before you plan the upgrade.
  • Test in a fresh virtual environment with deprecation warnings turned into errors.

FAQ

How much faster is Python 3.11?

The official release notes say Python 3.11 is between 10 and 60 percent faster than Python 3.10, with an average 1.25x speedup on the standard benchmark suite. Programs that mostly wait on I/O or run inside C extensions gain less.

What are the main new features in Python 3.11?

The main ones are a faster interpreter, fine-grained error locations in tracebacks, exception groups with except*, asyncio.TaskGroup, the tomllib module, typing.Self, and enum.StrEnum.

Is Python 3.11 backward compatible with 3.10?

Mostly. Almost all 3.10 code runs unchanged. A few long-deprecated items were removed, such as @asyncio.coroutine and inspect.getargspec(), and compiled dependencies need 3.11 builds.

Should I upgrade to Python 3.11 now?

Upgrade when all your dependencies publish 3.11 wheels and your test suite passes on it. If a compiled dependency is not ready, stay on 3.10 and add 3.11 to CI in the meantime.

What is except* in Python 3.11?

except* handles exceptions inside an ExceptionGroup. Each clause receives a group that contains only the matching exceptions, so you can handle several simultaneous errors from concurrent tasks.

Check your wheels, then take the free speed

Python 3.11 gives you a faster interpreter and better diagnostics without asking for code changes. The work is in verifying dependencies and running your tests on the new version. Do that in a clean environment, measure your real workload, and roll out in stages.

Rule of thumb: your code is probably ready for a new Python on release day, and your dependencies decide when you actually get there.

Share this article

Leave a Reply

Your email address will not be published. Required fields are marked *