Python · Python 3.10 and 3.11

Python 3.11: Exception Groups, Speed and tomllib

Where 3.11's speed came from, exception groups and except*, exception notes, tomllib, Self and StrEnum - and the enum formatting change that breaks upgrades.

  • 5 min read
  • 11 practice questions
On this page
  1. Description
  2. Faster CPython
  3. Exception groups and except*
  4. tomllib, Self and StrEnum
  5. Which tool for which job?
  6. Common mistakes
  7. Check yourself
  8. Key takeaways
  9. Quick revise
  10. Practice

Description

Python 3.11 is a big release: a much faster interpreter, exception groups and except* for code that fails in several places at once, exception notes, tomllib for reading TOML, and a few typing and enum additions. It's also a release whose compatibility notes matter as much as its features - one small change to how enums format has broken real services on upgrade.

Here's that bug. A service upgrades from Python 3.10 to 3.11 and gets faster for free - CPU time on its hot paths drops by about a fifth. Then a downstream system starts rejecting its messages. The payload is built with an f-string from an enum:

class Status(str, Enum):
    PAID = "paid"

body = f'{{"status": "{status}"}}'      # 3.10: {"status": "paid"}   3.11: {"status": "Status.PAID"}

Python 3.11 changed how enums that mix in str (or int) format: format(), f-strings and str() now give Status.PAID, like plain enums, instead of the value (a test printed Color.RED for a (str, Enum) member). StrEnum, also new in 3.11, formats as its value.

We'll go from simple to more technical:

  1. Faster CPython: where the speed came from.
  2. Exception groups and except*.
  3. Exception notes with add_note.
  4. tomllib, Self and StrEnum - and the enum change above.

Faster CPython

The Faster CPython project's first release made Python 3.11 about 25% faster on average (10-60% across the standard benchmark suite) with no code changes:

  • a specializing adaptive interpreter (PEP 659) rewrites hot bytecode into type-specialized versions (LOAD_ATTR for an instance attribute, BINARY_OP for two ints) and falls back if the types change;
  • cheaper function calls and frames, and lazily created frame objects;
  • zero-cost exceptions: a try block costs nothing when no exception is raised.

It's still an interpreter - CPU-heavy loops are still far slower than Java or C - but it changed the advice from "rewrite hot paths in C" to "upgrade first". 3.12 to 3.14 continued with smaller gains and an experimental JIT.

Exception groups and except*

Concurrent code can fail in several ways at once - a TaskGroup whose two tasks both raise. Python 3.11 (PEP 654) added ExceptionGroup to carry them together, and except* to handle them by type:

try:
    async with asyncio.TaskGroup() as tg:
        ...
except* ValueError as eg:          # eg is an ExceptionGroup of just the ValueErrors
    log.warning("bad input: %s", eg.exceptions)
except* KeyError as eg:
    ...
  • Each except* clause takes the members that match and receives them as a (sub)group; several clauses can run for one group - a group of two ValueErrors and a KeyError ran both handlers in a test.
  • Unhandled members propagate as a new ExceptionGroup with the same message: handling only ValueError let ExceptionGroup('batch failed', [KeyError('sku')]) escape.
  • except and except* can't be mixed on one try (SyntaxError: cannot have both 'except' and 'except*' on the same 'try'), and a plain except ValueError doesn't match a group containing one - which is why code around a TaskGroup needs except*.

Exception notes (PEP 678): err.add_note("while parsing row 3 of orders.csv") attaches context to any exception and the traceback prints it after the message - a test's traceback ended with ValueError: invalid literal for int() ... and then the note. Better than wrapping exceptions just to add context.

tomllib, Self and StrEnum

  • tomllib (PEP 680) parses TOML - the format of pyproject.toml - into dicts. It's read-only (there's no dump; use a third-party library to write), and files must be opened in binary mode: a text-mode file gives TypeError: File must be opened in binary mode, e.g. use `open('foo.toml', 'rb')` . tomllib.loads(text) parses a string.
  • typing.Self annotates methods that return their own class (def with_id(self, id) -> Self:), so subclasses get the right type.
  • enum.StrEnum members are strings that format as their value - the fix for the formatting change above. For a (str, Enum) you can't change, format .value explicitly. (Comparisons were never affected: Color.RED == "red" is still True.)
  • Also in 3.11: fine-grained traceback locations (carets under the exact sub-expression that failed), and asyncio.TaskGroup and asyncio.timeout (see the asyncio module).

Which tool for which job?

You want toUse
Handle errors from a TaskGroup or a batchexcept* SomeError as eg - eg.exceptions
Handle a single, ordinary exceptionplain except
Add context to an exception without wrapping iterr.add_note("...")
Read pyproject.toml or another TOML filetomllib.load(open(path, "rb"))
Write TOMLa third-party library (tomli-w)
An enum whose members format as their valueStrEnum
A method that returns its own class-> Self

Common mistakes

MistakeWhat happensDo this instead
f-strings on (str, Enum) members after upgradingStatus.PAID instead of paidStrEnum, or .value
except ValueError around a TaskGroupnever matches the groupexcept* ValueError
except and except* on one trySyntaxErrorone kind per try
Expecting only one except* clause to runother handlers run toowrite each clause for its own members
tomllib.load on a text-mode fileTypeErroropen with "rb"
Wrapping an exception only to add contexta longer, noisier tracebackadd_note

Check yourself

Try to answer before opening each one - recalling beats re-reading.

1. A group holds two ValueErrors and a KeyError. The try has except* ValueError and except* KeyError. Which handlers run?

Show answer

Both: each except* clause takes the matching members of the group. The ValueError handler gets a group of two.

2. Why did f"{Status.PAID}" change from paid to Status.PAID after upgrading to 3.11?

Show answer

Python 3.11 changed format() and str() of mixed-in enums like class Status(str, Enum) to match plain enums. Use StrEnum, or format Status.PAID.value.

3. Why does tomllib.load(open("app.toml")) fail?

Show answer

tomllib requires a binary file: open("app.toml", "rb"). (Or read the text and use tomllib.loads.)

Key takeaways

  • Upgrade to 3.11+ for speed alone - and read the "Changes in the Python API" notes first.
  • ExceptionGroup + except*: per-type handling, several clauses, leftovers re-raised.
  • add_note for context; tomllib reads TOML from binary files.
  • (str, Enum) formatting changed in 3.11 - prefer StrEnum.

Quick revise

The whole skill on one page, for the night before an interview.

Everything from the lesson and the deep dive on one page. Skim it before practice or an interview, and open the full lesson for anything that doesn't ring a bell.

Pick the tool

You want toUse
Handle errors from several concurrent tasksexcept* SomeError as eg
Add context without wrappingerr.add_note("...")
Read TOMLtomllib.load(f) with open(path, "rb"), or tomllib.loads(text)
Enum that formats as its valueStrEnum (or serialize .value)
Return your own class from a method-> Self
UTC as a constantdatetime.UTC

Snippets worth knowing by heart

try:
    async with asyncio.TaskGroup() as tg:
        tg.create_task(load_orders())
        tg.create_task(load_users())
except* ValueError as eg:
    log.warning("bad input: %s", eg.exceptions)
except* KeyError as eg:
    log.warning("missing keys: %s", eg.exceptions)

try:
    qty = int(cell)
except ValueError as err:
    err.add_note(f"row {row} of {path}")
    raise

with open("pyproject.toml", "rb") as f:
    config = tomllib.load(f)

The traps

  • (str, Enum) in f-strings since 3.11 - Status.PAID, not paid.
  • except ValueError around a TaskGroup - never matches; it's a group.
  • except and except* together - SyntaxError.
  • Expecting one except* handler - every matching clause runs.
  • tomllib with a text-mode file - TypeError; there's no dump.

Staff-level points, one line each

  • PEP 659 rewrites hot bytecode in place: BINARY_OP became BINARY_OP_ADD_INT, then BINARY_OP_ADD_FLOAT when floats arrived.
  • Type-stable code specializes best; benchmarks need a warm-up.
  • Zero-cost try: an exception table, read only when something is raised - a try in a loop cost nothing, raising every time cost about 4x.
  • except* uses split(), keeping nested groups; leftovers re-raise with the same message.
  • A naked exception reaches except* wrapped: ExceptionGroup('', (ValueError('x'),)).
  • No return/break/continue in except*; except* ExceptionGroup is a TypeError.
  • Custom groups override derive().
  • 3.11+: str()/format() of (str, Enum) give Class.MEMBER; IntEnum gives the number; equality unchanged.
  • tomllib gives datetime/date objects and exact big ints.

Say it in an interview

  • "Moving to 3.11 is the cheapest speed-up we have - but we read the API changes first."
  • "Around a TaskGroup I catch with except*, one clause per error type."
  • "I'd add a note with the row number rather than wrapping the exception."
  • "We serialize enums by .value, so formatting changes can't leak into payloads."

Practice

This skill comes with 11 multiple-choice questions that tell you which misconception a wrong answer reveals. Your answers are graded and your mastery is tracked, so you'll know when the skill holds up. Running code and saving progress need a free account.

Practice Python 3.11: Exception Groups, Speed and tomllib free

Practice Python 3.11: Exception Groups, Speed and tomllib free

No card needed. Lessons, practice questions and easy problems are free forever.

Free plan, no card · 7-day Pro trial · Cancel anytime