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.
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
tryblock 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.
- Install 3.11 side by side. Keep your current interpreter. Confirm the new one with
python3.11 --version. - 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
- Watch the install output. A line such as
Building wheel for some-packagemeans 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. - Run your tests with deprecation warnings as errors. This surfaces code that will break in 3.12.
python -W error::DeprecationWarning -m pytest
- Benchmark your own workload on both versions, as shown earlier.
- Add 3.11 to your CI matrix beside 3.10, and keep both until production has moved.
- 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
cp311wheel. 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
- Do all your compiled dependencies ship 3.11 wheels? If not, wait. Everything else is secondary.
- Is your workload CPU-bound in pure Python? You gain the most. Upgrade as soon as the tests pass.
- Do you write concurrent
asynciocode?TaskGroupandexcept*fix real error-handling gaps. That alone justifies the move. - Do you maintain a library? Add 3.11 to your test matrix now. Your users cannot upgrade until you do.
- 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 aSyntaxErroron older versions, so the whole module fails to import there. - Opening TOML files in text mode.
tomllib.load()requires binary mode and raisesTypeErrorotherwise. - Ignoring deprecation warnings. Modules deprecated now disappear in 3.12 and 3.13. The warning you skip today becomes an
ImportErrorlater. - Forgetting the type checker. An old mypy or pyright rejects
SelfandNotRequired. 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*, andasyncio.TaskGroupmake concurrent error handling correct.tomllib,typing.Self, andStrEnumremove 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.
