Right, let’s talk about Python 3.11. This is the release where the core team looked at the interpreter, saw it was good, but decided “good” wasn’t good enough. They went at it with a performance profiler and a mandate to make things fast. But they also snuck in a few language features that are, frankly, a bit of a big deal. We’ll get to the speed in a minute, but we have to start with the headliner: a better way to handle multiple errors at once. Because sometimes, failure isn’t a single event; it’s a whole catastrophe.

Exception Groups and except*

Let’s say you’re running a bunch of tasks concurrently and several of them blow up. Pre-3.11, you’d typically get the first exception that got raised, and the others would be lost to the ether, leaving you to play detective. This was, in a word, rubbish. Python 3.11 introduces ExceptionGroup and the new except* syntax to finally fix this.

An ExceptionGroup is exactly what it sounds like: a container that holds multiple exceptions. You don’t catch it with a regular except; you use except* to simultaneously catch the group and filter the specific exception types inside it. It’s like saying, “Give me all the ValueError instances from this bundle of failures.”

def task_one():
    raise ValueError("task_one had a value issue")

def task_two():
    raise TypeError("task_one had a type issue")

def task_three():
    raise ValueError("task_three also had a value issue")

exceptions = []
for func in [task_one, task_two, task_three]:
    try:
        func()
    except Exception as e:
        exceptions.append(e)

if exceptions:
    raise ExceptionGroup("Several tasks failed", exceptions)

Now, let’s catch the specific errors from the group:

try:
    # ... some operation that raises the ExceptionGroup above
    pass
except* ValueError as eg:
    print(f"Got {len(eg.exceptions)} ValueErrors: {eg.exceptions}")
except* TypeError as eg:
    print(f"Got a TypeError: {eg.exceptions}")
# This will output:
# Got 2 ValueErrors: [ValueError('task_one had a value issue'), ValueError('task_three also had a value issue')]
# Got a TypeError: [TypeError('task_one had a type issue')]

The magic here is that all exceptions in the group are processed. The except* ValueError clause grabs every ValueError from the group, and execution continues so the except* TypeError can do the same. If you had a fourth KeyError in the group that wasn’t caught, it would wrap the remaining exceptions into a new ExceptionGroup and raise that. This is a game-changer for async code and parallel processing, making error handling actually sane.

The tomllib Module: Finally.

If you’ve ever written a tool that reads a pyproject.toml file and wept at the need to install an external dependency (toml), weep no more. Python 3.11 incorporates tomllib, a parser for TOML. Notice the extra ’l’. It’s a standard library module, and it’s basically a clone of the excellent tomli library. There’s one crucial thing to remember: it’s read-only.

import tomllib

toml_data = """
[project]
name = "my-awesome-project"
version = "1.0.0"
dependencies = ["requests", "rich"]

[tool.black]
line-length = 88
"""

config = tomllib.loads(toml_data)
print(config["project"]["name"])  # Output: my-awesome-project
print(config["tool"]["black"]["line-length"])  # Output: 88

# To write TOML, you still need tomli-w or another library.
# tomllib only reads.

Why is it read-only? Because the standards committee decided that serializing to TOML was a complex enough problem that it shouldn’t be rushed into the standard library. A fair point, but it means for a full TOML workflow, you’ll still need an external dependency for writing. It’s a half-victory, but a welcome one.

The Interpreter Speed Boost: “Faster CPython”

This isn’t just a minor optimization; it’s a multi-year project called “Faster CPython” that’s starting to pay serious dividends. The headline is that 3.11 is often 25-60% faster than 3.10. How? Not by inventing new algorithms, but by optimizing the living daylights out of the interpreter itself.

The key innovation is specialization adaptive interpreter. Fancy term. Here’s what it means: The interpreter now profiles your bytecode as it runs. For common instruction pairs, it replaces the general-purpose bytecode with specialized, faster versions that skip unnecessary steps. The classic example is a binary operation: instead of having one opcode that checks the types of both operands every single time, it can create a specialized opcode that assumes both operands are integers, making it blindingly fast. If that assumption is ever wrong, it bails back to the general-purpose version. This is why the speedup is most noticeable in “boring”, compute-heavy code—tight loops, arithmetic, function calls. It’s not magic; it’s just exceptionally clever engineering.

# A dumb, simple loop that benefits massively from the specialization.
def calculate(n):
    total = 0
    for i in range(n):
        total += i
    return total

# Time this in 3.10 vs 3.11. The difference is startling.

The best part? You get this for free. No code changes, no new APIs to learn. Just upgrade and enjoy your code running noticeably faster. It’s the closest thing to a free lunch you’ll get in computing. The team has been meticulous about ensuring these optimizations don’t change semantics or break existing code. So far, they’ve succeeded brilliantly.