Movatterモバイル変換


[0]ホーム

URL:


— FREE Email Series —

🐍 Python Tricks 💌

Python Tricks Dictionary Merge

🔒 No spam. Unsubscribe any time.

Browse TopicsGuided Learning Paths
Basics Intermediate Advanced
apibest-practicescareercommunitydatabasesdata-sciencedata-structuresdata-vizdevopsdjangodockereditorsflaskfront-endgamedevguimachine-learningnumpyprojectspythontestingtoolsweb-devweb-scraping

Table of Contents

Python 3.11 Preview: Task and Exception Groups

Python 3.11 Preview: Task and Exception Groups

byGeir Arne Hjelleadvancedpython

Table of Contents

Remove ads

Python 3.11 will be released in October 2022. Even though October is still months away, you can already preview some of the upcoming features, including the new task and exception groups that Python 3.11 has to offer. Task groups let you organize your asynchronous code better, while exception groups can collect several errors happening at the same time and let you handle them in a straightforward manner.

In this tutorial, you’ll:

  • Install Python 3.11 alpha on your computer, next to your current Python installations
  • Explore howexception groups can organize several unrelated errors
  • Filter exception groups withexcept* andhandle different types of errors
  • Usetask groups to set up yourasynchronous code
  • Test smaller improvements in Python 3.11, includingexception notes and a new internalrepresentation of exceptions

There are many other improvements and features coming in Python 3.11. Check outwhat’s new in the changelog for an up-to-date list.

Free Download:Click here to download free sample code that demonstrates some of the new features of Python 3.11.

Python 3.11 Alpha

A new version of Python is released in October each year. The code is developed and tested over aseventeen-month period before the release date. New features are implemented during thealpha phase, which lasts until May, about five months before the final release.

Aboutonce a month during the alpha phase, Python’s core developers release a newalpha version to show off the new features, test them, and get early feedback. Currently, the latest alpha version of Python 3.11 is3.11.0a7, released on April 5, 2022.

Note: This tutorial uses the seventh alpha version of Python 3.11. You might experience small differences if you use a later version. In particular, a few aspects of the task group implementation are still being discussed. However, you can expect most of what you learn here to stay the same through the alpha and beta phases and in the final release of Python 3.11.

The firstbeta release of Python 3.11 is just around the corner, planned for May 6, 2022. Typically, no new features are added during thebeta phase. Instead, the time between the feature freeze and the release date is used to test and solidify the code.

Cool New Features

Some of the currently announced highlights of Python 3.11 include:

  • Exception groups, which will allow programs to raise and handle multiple exceptions at the same time
  • Task groups, to improve how you run asynchronous code
  • Enhanced error messages, which will help you more effectively debug your code
  • Optimizations, promising to make Python 3.11 significantly faster than previous versions
  • Static typing improvements, which will let youannotate your code more precisely
  • TOML support, which allows you to parse TOML documents using the standard library

There’s a lot to look forward to in Python 3.11! For a comprehensive overview, check outPython 3.11: Cool New Features for You to Try. You can also dive deeper into some of the features listed above in the other articles in this series:

In this tutorial, you’ll focus on how exception groups can handle multiple unrelated exceptions at once and how this feature paves the way for task groups, which make concurrent programming in Python more convenient. You’ll also get a peek at some of the other, smaller features that’ll be shipping with Python 3.11.

Installation

To play with the code examples in this tutorial, you’ll need to install a version of Python 3.11 onto your system. In this subsection, you’ll learn about a few different ways to do this: usingDocker, usingpyenv, or installing fromsource. Pick the one that works best for you and your system.

Note: Alpha versions are previews of upcoming features. While most features will work well, you shouldn’t depend on any Python 3.11 alpha version in production or anywhere else where potential bugs will have serious consequences.

If you have access toDocker on your system, then you can download the latest version of Python 3.11 by pulling and running thepython:3.11-rc-slimDocker image:

Shell
$dockerpullpython:3.11-rc-slimUnable to find image 'python:3.11-rc-slim' locallylatest: Pulling from library/python[...]$dockerrun-it--rmpython:3.11-rc-slim

This drops you into a Python 3.11 REPL. Check outRun Python Versions in Docker for more information about working with Python through Docker, including how to run scripts.

Thepyenv tool is great for managing different versions of Python on your system, and you can use it to install Python 3.11 alpha if you like. It comes with two different versions, one for Windows and one for Linux and macOS. Choose your platform with the switcher below:

On Windows, you can usepyenv-win. First update yourpyenv installation:

Windows PowerShell
PS>pyenvupdate:: [Info] ::  Mirror: https://www.python.org/ftp/python[...]

Doing an update ensures that you can install the latest version of Python. You could alsoupdatepyenv manually.

On Linux and macOS, you can usepyenv. First update yourpyenv installation, using thepyenv-update plugin:

Shell
$pyenvupdateUpdating /home/realpython/.pyenv...[...]

Doing an update ensures that you can install the latest version of Python. If you don’t want to use the update plugin, you canupdatepyenv manually.

Usepyenv install --list to check which versions of Python 3.11 are available. Then, install the latest one:

Shell
$pyenvinstall3.11.0a7Downloading Python-3.11.0a7.tar.xz...[...]

The installation may take a few minutes. Once your new alpha version is installed, then you can create avirtual environment where you can play with it:

Windows PowerShell
PS>pyenvlocal3.11.0a7PS>python--versionPython 3.11.0a7PS>python-mvenvvenvPS>venv\Scripts\activate

You usepyenv local to activate your Python 3.11 version, and then set up the virtual environment withpython -m venv.

Shell
$pyenvvirtualenv3.11.0a7311_preview$pyenvactivate311_preview(311_preview)$python--versionPython 3.11.0a7

On Linux and macOS, you use thepyenv-virtualenv plugin to set up the virtual environment and activate it.

You can also install Python from one of pre-release versions available onpython.org. Choose thelatest pre-release and scroll down to theFiles section at the bottom of the page. Download and install the file corresponding to your system. SeePython 3 Installation & Setup Guide for more information.

Many of the examples in this tutorial will work on older versions of Python, but in general, you should run them with your Python 3.11 executable. Exactly how you run the executable depends on how you installed it. If you need help, see the relevant tutorial onDocker,pyenv,virtual environments, orinstalling from source.

Exception Groups andexcept* in Python 3.11

Dealing withexceptions is an important part of programming. Sometimes errors happen because of bugs in your code. In those cases,good error messages will help you debug your code efficiently. Other times, errors happen through no fault of your code. Maybe the user tries to open a corrupt file, maybe the network is down, or maybe authentication to a database is missing.

Usually, only one error happens at a time. It’s possible that another error would’ve happened if your code had continued to run. But Python will typically only report the first error it encounters. There are situations where it makes sense to report several bugs at once though:

  • Several concurrent tasks can fail at the same time.
  • Cleanup code can cause its own errors.
  • Code can try several different alternatives that all raise exceptions.

In Python 3.11, a new feature calledexception groups is available. It provides a way to group unrelated exceptions together, and it comes with a newexcept* syntax for handling them. A detailed description is available inPEP 654: Exception Groups andexcept*.

PEP 654 has been written and implemented byIrit Katriel, one of CPython’s core developers, with support fromasyncio maintainerYury Selivanov and former BDFLGuido van Rossum. It was presented and discussed at thePython Language Summit in May 2021.

This section will teach you how to work with exception groups. In thenext section, you’ll see a practical example of concurrent code that uses exception groups to raise and handle errors from several tasks simultaneously.

Handle Regular Exceptions Withexcept

Before you explore exception groups, you’ll review how regular exception handling works in Python. If you’re already comfortable handling errors in Python, you won’t learn anything new in this subsection. However, this review will serve as a contrast to what you’ll learn about exception groups later. Everything you’ll see in this subsection of the tutorial works in all versions of Python 3, including Python 3.10.

Exceptions break the normal flow of a program. If an exception is raised, then Python drops everything else and looks for code that handles the error. If there are no such handlers, then the program stops, regardless of what the program was doing.

You can raise an error yourself using theraise keyword:

Python
>>>raiseValueError(654)Traceback (most recent call last):...ValueError:654

Here, you explicitly raise aValueError with the description654. You can see that Python provides atraceback, which tells you that there’s an unhandled error.

Sometimes, you raise errors like this in your code to signal that something has gone wrong. However, it’s more common to encounter errors raised by Python itself or some library that you’re using. For example, Python doesn’t let you add a string and an integer, and raises aTypeError if you attempt this:

Python
>>>"3"+11Traceback (most recent call last):...TypeError:can only concatenate str (not "int") to str

Most exceptions come with a description that can help you figure out what went wrong. In this case, it tells you that your second term should also be a string.

You usetryexcept blocks to handle errors. Sometimes, you use these to just log the error and continue running. Other times, you manage to recover from the error or calculate some alternative value instead.

A shorttryexcept block may look as follows:

Python
>>>try:...raiseValueError(654)...exceptValueErroraserr:...print(f"Got a bad value:{err}")...Got a bad value: 654

You handleValueError exceptions by printing a message to your console. Note that because you handled the error, there’s no traceback in this example. However, other types of errors aren’t handled:

Python
>>>try:..."3"+11...exceptValueErroraserr:...print(f"Got a bad value:{err}")...Traceback (most recent call last):...TypeError:can only concatenate str (not "int") to str

Even though the error happens within atryexcept block, it’s not handled because there’s noexcept clause that matches aTypeError. You can handle several kinds of errors in one block:

Python
>>>try:..."3"+11...exceptValueErroraserr:...print(f"Got a bad value:{err}")...exceptTypeErroraserr:...print(f"Got bad types:{err}")...Got bad types: can only concatenate str (not "int") to str

This example will handle bothValueError andTypeError exceptions.

Exceptions are defined in ahierarchy. For example, aModuleNotFoundError is a kind ofImportError, which is a kind ofException.

Note: Because most exceptions inherit fromException, you could try to simplify your error handling by using onlyexcept Exception blocks. This is usually a bad idea. You want your exception blocks to be as specific as possible, to avoid unexpected errors occurring and messing up your error handling.

The firstexcept clause that matches the error will trigger the exception handling:

Python
>>>try:...importno_such_module...exceptImportErroraserr:...print(f"ImportError:{err.__class__}")...exceptModuleNotFoundErroraserr:...print(f"ModuleNotFoundError:{err.__class__}")...ImportError: <class 'ModuleNotFoundError'>

When you try to import a module that doesn’t exist, Python raises aModuleNotFoundError. However, sinceModuleNotFoundError is a kind ofImportError, your error handling triggers theexcept ImportError clause. Note that:

  • At most oneexcept clause will trigger
  • The firstexcept clause that matches will trigger

If you’ve worked with exceptions before, this may seem intuitive. However, you’ll see later that exception groups behave differently.

While at most one exception is active at a time, it’s possible to chain related exceptions. This chaining was introduced byPEP 3134 for Python 3.0. As an example, observe what happens if you raise a new exception while handling an error:

Python
>>>try:..."3"+11...exceptTypeError:...raiseValueError(654)...Traceback (most recent call last):...TypeError:can only concatenate str (not "int") to strDuring handling of the above exception, another exception occurred:Traceback (most recent call last):...ValueError:654

Note the lineDuring handling of the above exception, another exception occurred. There’s one traceback before this line, representing the originalTypeError caused by your code. Then, there’s another traceback below the line, representing the newValueError that you raised while handling theTypeError.

This behavior is particularly useful if you happen to have an issue in your error handling code, because you then get information about both your original error and the bug in your error handler.

You can also explicitly chain exceptions together yourself using araisefrom statement. While you can use chained exceptions to raise several exceptions at once, note that the mechanism is meant for exceptions that are related, specificially where one exception happens during the handling of another.

This is different from the use case that exception groups are designed to handle. Exception groups will group together exceptions that are unrelated, in the sense that they happen independently of each other. When handling chained exceptions, you’re only able to catch and handle the last error in the chain. As you’ll learn soon, you can catch all the exceptions in an exception group.

Group Exceptions WithExceptionGroup

In this subsection, you’ll explore the newExceptionGroup class that’s available in Python 3.11. First, note that anExceptionGroup is also a kind ofException:

Python
>>>issubclass(ExceptionGroup,Exception)True

AsExceptionGroup is a subclass ofException, you can use Python’s regular exception handling to work with it. You can raise anExceptionGroup withraise, although you probably won’t do that very often unless you’re implementing some low-level library. It’s also possible to catch anExceptionGroup withexcept ExceptionGroup. However, as you’ll learn in thenext subsection, you’re usually better off using the newexcept* syntax.

In contrast to most other exceptions, exception groups take two arguments when they’re initialized:

  1. The usual description
  2. A sequence of sub-exceptions

The sequence of sub-exceptions can include other exception groups, but it can’t be empty:

Python
>>>ExceptionGroup("one error",[ValueError(654)])ExceptionGroup('one error', [ValueError(654)])>>>ExceptionGroup("two errors",[ValueError(654),TypeError("int")])ExceptionGroup('two errors', [ValueError(654), TypeError('int')])>>>ExceptionGroup("nested",...[...ValueError(654),...ExceptionGroup("imports",...[...ImportError("no_such_module"),...ModuleNotFoundError("another_module"),...]...),...]...)ExceptionGroup('nested', [ValueError(654), ExceptionGroup('imports',  [ImportError('no_such_module'), ModuleNotFoundError('another_module')])])>>>ExceptionGroup("no errors",[])Traceback (most recent call last):...ValueError:second argument (exceptions) must be a non-empty sequence

In this example, you’reinstantiating a few different exception groups that show that exception groups can contain one exception, several exceptions, and even other exception groups. Exception groups aren’t allowed to be empty, though.

Your first encounter with an exception group is likely to be its traceback. Exception group tracebacks are formatted to clearly show you the structure within the group. You’ll see a traceback when you raise an exception group:

Python
>>>raiseExceptionGroup("nested",...[...ValueError(654),...ExceptionGroup("imports",...[...ImportError("no_such_module"),...ModuleNotFoundError("another_module"),...]...),...TypeError("int"),...]...)  + Exception Group Traceback (most recent call last):  |   ...  | ExceptionGroup: nested (3 sub-exceptions)  +-+---------------- 1 ----------------    | ValueError: 654    +---------------- 2 ----------------    | ExceptionGroup: imports (2 sub-exceptions)    +-+---------------- 1 ----------------      | ImportError: no_such_module      +---------------- 2 ----------------      | ModuleNotFoundError: another_module      +------------------------------------    +---------------- 3 ----------------    | TypeError: int    +------------------------------------

The traceback lists all exception that are part of an exception group. Additionally, the nested tree structure of exceptions within the group is indicated, both graphically and by listing how many sub-exceptions there are in each group.

You learned earlier thatExceptionGroup doubles as a regular Python exception. This means that you can catch exception groups with regularexcept blocks:

Python
>>>try:...raiseExceptionGroup("group",[ValueError(654)])...exceptExceptionGroup:...print("Handling ExceptionGroup")...Handling ExceptionGroup

This usually isn’t very helpful, because you’re more interested in the errors that are nested inside the exception group. Note that you’re not able to directly handle those:

Python
>>>try:...raiseExceptionGroup("group",[ValueError(654)])...exceptValueError:...print("Handling ValueError")...  + Exception Group Traceback (most recent call last):  |   ...  | ExceptionGroup: group (1 sub-exception)  +-+---------------- 1 ----------------    | ValueError: 654    +------------------------------------

Even though the exception group contains aValueError, you’re not able to handle it withexcept ValueError. Instead, you should use a newexcept* syntax to handle exception groups. You’ll learn how that works in the next section.

Filter Exceptions Withexcept*

There have been attempts at handling multiple errors in earlier versions of Python. For example, the popularTrio library includes aMultiError exception that can wrap other exceptions. However, because Python is primed toward handling one error at a time, dealing withMultiError exceptions isless than ideal.

The newexcept* syntax in Python 3.11 makes it more convenient to gracefully deal with several errors at the same time. Exception groups have a few attributes and methods that regular exceptions don’t have. In particular, you can access.exceptions to obtain a tuple of all sub-exceptions in the group. You could, for example, rewrite the last example in the previous subsection as follows:

Python
>>>try:...raiseExceptionGroup("group",[ValueError(654)])...exceptExceptionGroupaseg:...forerrineg.exceptions:...ifisinstance(err,ValueError):...print("Handling ValueError")...elifisinstance(err,TypeError):...print("Handling TypeError")...Handling ValueError

Once you catch anExceptionGroup, you loop over all the sub-exceptions and handle them based on their type. While this is possible, it quickly gets cumbersome. Also note that the code above doesn’t handle nested exception groups.

Instead, you should useexcept* to handle exception groups. You can rewrite the example once more:

Python
>>>try:...raiseExceptionGroup("group",[ValueError(654)])...except*ValueError:...print("Handling ValueError")...except*TypeError:...print("Handling TypeError")...Handling ValueError

Eachexcept* clause handles an exception group that’s a subgroup of the original exception group, containing all exceptions matching the given type of error. Consider the slightly more involved example:

Python
>>>try:...raiseExceptionGroup(..."group",[TypeError("str"),ValueError(654),TypeError("int")]...)...except*ValueErroraseg:...print(f"Handling ValueError:{eg.exceptions}")...except*TypeErroraseg:...print(f"Handling TypeError:{eg.exceptions}")...Handling ValueError: (ValueError(654),)Handling TypeError: (TypeError('str'), TypeError('int'))

Note that in this example, bothexcept* clauses trigger. This is different from regularexcept clauses, where at most one clause triggers at a time.

First theValueError is filtered from the original exception group and handled. TheTypeError exceptions remain unhandled until they’re caught byexcept* TypeError. Each clause is only triggered once, even if there are more exceptions of that type. Your handling code must therefore deal with exception groups.

You may end up only partially handling an exception group. For example, you could handle onlyValueError from the previous example:

Python
>>>try:...raiseExceptionGroup(..."group",[TypeError("str"),ValueError(654),TypeError("int")]...)...except*ValueErroraseg:...print(f"Handling ValueError:{eg.exceptions}")...Handling ValueError: (ValueError(654),)  + Exception Group Traceback (most recent call last):  |   ...  | ExceptionGroup: group (2 sub-exceptions)  +-+---------------- 1 ----------------    | TypeError: str    +---------------- 2 ----------------    | TypeError: int    +------------------------------------

In this case, theValueError is handled. But that leaves two unhandled errors in the exception group. Those errors then bubble out and create a traceback. Note that theValueError is not part of the traceback because it’s already been handled. You can see thatexcept* behaves differently fromexcept:

  • Severalexcept* clauses may trigger.
  • except* clauses that match an error remove that error from the exception group.

This is a clear change from how plainexcept works, and may feel a bit unintuitive at first. However, the changes make it more convenient to deal with multiple concurrent errors.

You can—although you probably don’t need to—split exception groups manually:

Python
>>>eg=ExceptionGroup(..."group",[TypeError("str"),ValueError(654),TypeError("int")]...)>>>egExceptionGroup('group', [TypeError('str'), ValueError(654), TypeError('int')])>>>value_errors,eg=eg.split(ValueError)>>>value_errorsExceptionGroup('group', [ValueError(654)])>>>egExceptionGroup('group', [TypeError('str'), TypeError('int')])>>>import_errors,eg=eg.split(ImportError)>>>print(import_errors)None>>>egExceptionGroup('group', [TypeError('str'), TypeError('int')])>>>type_errors,eg=eg.split(TypeError)>>>type_errorsExceptionGroup('group', [TypeError('str'), TypeError('int')])>>>print(eg)None

You can use.split() on exception groups to split them into two new exception groups. The first group consists of errors that match a given error, while the second group consists of those errors that are left over. If any of the groups end up empty, then they’re replaced byNone. SeePEP 654 and thedocumentation for more information if you want to manually manipulate exception groups.

Exception groups won’t replace regular exceptions! Instead, they’re designed to handle the specific use case where it’s useful to deal with several exceptions at the same time. Libraries should clearly differentiate between functions that can raise regular exceptions and functions that can raise exception groups.

The authors of PEP 654 recommend that changing a function from raising an exception to raising an exception group should be considered a breaking change because anyone using that library needs to update how they handle errors. In the next section, you’ll learn about task groups. They’re new in Python 3.11 and are the first part of the standard library to raise exception groups.

You’ve seen that it’s possible, but cumbersome, to deal with exception groups within regularexcept blocks. It’s also possible to do the opposite.except* can handle regular exceptions:

Python
>>>try:...raiseValueError(654)...except*ValueErroraseg:...print(type(eg),eg.exceptions)...<class 'ExceptionGroup'> (ValueError(654),)

Even though you raise a singleValueError exception, theexcept* mechanism wraps the exception in an exception group before handling it. In theory, this means that you can replace all yourexcept blocks withexcept*. In practice, that would be a bad idea. Exception groups are designed to handle multiple exceptions. Don’t use them unless you need to!

Exception groups are new in Python 3.11. However, if you’re using an older version of Python, then you can use theexceptiongroup backport to access the same functionality. Instead ofexcept*, the backport uses anexceptiongroup.catch()context manager to handle multiple errors. You can learn more about catching multiple exceptions inHow to Catch Multiple Exceptions in Python.

Asynchronous Task Groups in Python 3.11

You learned about exception groups in the previous section. When would you use them? As noted, exception groups andexcept* aren’t meant to replace regular exceptions andexcept.

In fact, chances are that you don’t have a good use case for raising exception groups in your own code. They’ll likely be used mostly in low-level libraries. As Python 3.11 gets more widespread, packages that you rely on may start raising exception groups, so you may need to handle them in your applications.

One of the motivating use cases for introducing exception groups is dealing with errors in concurrent code. If you have several tasks running at the same time, several of them may run into issues. Until now, Python hasn’t had a good way of dealing with that. Several asynchronous libraries, likeTrio,AnyIO andCurio, have added a kind of multi-error container. But without language support, it’s still complicated to handle concurrent errors.

If you’d like to see a video presentation of exception groups and their use in concurrent programming, have a look atŁukasz Langa’s presentationHow Exception Groups Will Improve Error Handling in AsyncIO.

In this section, you’ll explore a toy example that simulates analyzing several files concurrently. You’ll build the example from a basic synchronous application where the files are analyzed in sequence up to a full asynchronous tool that uses the new Python 3.11asyncio task groups. Similar task groups exist in other asynchronous libraries, but the new implementation is the first to use exception groups in order to smooth out error handling.

Your first versions of the analysis tool will work with older versions of Python, but you’ll need Python 3.11 to take advantage of task and exception groups in the final examples.

Analyze Files Sequentially

In this subsection, you’ll implement a tool that can count the number of lines in several files. The output will be animated so that you get a nice visual representation of the distribution of file sizes. The final result will look something like this:

Files being analyzed, one after another

You’ll expand this program to explore some features of asynchronous programming. While this tool isn’t necessarily useful on its own, it’s explicit so that you can clearly see what’s happening, and it’s flexible so that you can introduce several exceptions and work toward handling them with exception groups.

Colorama is a library that gives you more control of output in your terminal. You’ll use it to create an animation as your program counts the number of lines in the different files. First, install it withpip:

Shell
$python-mpipinstallcolorama

As the name suggests, Colorama’s primary use case is adding color to your terminal. However, you can also use it to print text at specific locations. Write the following code into a file namedcount.py:

Python
# count.pyimportsysimporttimeimportcoloramafromcoloramaimportCursorcolorama.init()defprint_at(row,text):print(Cursor.POS(1,1+row)+str(text))time.sleep(0.03)defcount_lines_in_file(file_num,file_name):counter_text=f"{file_name[:20]:<20} "withopen(file_name,mode="rt",encoding="utf-8")asfile:forline_num,_inenumerate(file,start=1):counter_text+="□"print_at(file_num,counter_text)print_at(file_num,f"{counter_text} ({line_num})")defcount_all_files(file_names):forfile_num,file_nameinenumerate(file_names,start=1):count_lines_in_file(file_num,file_name)if__name__=="__main__":count_all_files(sys.argv[1:])

Theprint_at() function is at the heart of the animation. It uses Colorama’sCursor.POS() to print some text at a particular row or line in your terminal. Next, itsleeps for a short while to create the animation effect.

You usecount_lines_in_file() to analyze and animate one file. The function opens a file and iterates through it, one line at a time. For each line, it adds a box () to a string and usesprint_at() to continually print the string on the same row. This creates the animation. At the end, the total number of lines is printed.

Note: Positioning your terminal cursor with Colorama is a quick way to create a simple animation. However, it does mess with the regular flow of your terminal, and you may experience some issues with text being overwritten.

You’ll have a smoother experience by clearing the screen before analyzing the files and by setting the cursor below your animation at the end. You can do this by adding something like the following to your main block:

Python
# count.py# ...if__name__=="__main__":print(colorama.ansi.clear_screen())count_all_files(sys.argv[1:])print(Cursor.POS(1,1+len(sys.argv)))

You can also change the number that’s added to the second argument ofCursor.POS() here and inprint_at() to get a behavior that plays nicely with your terminal setup. When you find a number that works, you should do similar customizations in later examples as well.

Your program’s entry point iscount_all_files(). This loops over all filenames that you provide ascommand-line arguments and callscount_lines_in_file() on them.

Try out your line counter! You run the program by providing files that should be analyzed on the command line. For example, you can count the number of lines in your source code as follows:

Shell
$pythoncount.pycount.pycount.py             □□□□□□□□□□□□□□□□□□□□□□□□□□□□ (28)

This counts the number of lines incount.py. You should create a few other files that you’ll use to explore your line counter. Some of these files will expose that you’re not doing any exception handling at the moment. You can create a few new files with the following code:

Python
>>>importpathlib>>>importstring>>>chars=string.ascii_uppercase>>>data=[c1+c2forc1,c2inzip(chars[:13],chars[13:])]>>>pathlib.Path("rot13.txt").write_text("\n".join(data))38>>>pathlib.Path("empty_file.txt").touch()>>>bbj=[98,108,229,98,230,114,115,121,108,116,101,116,248,121]>>>pathlib.Path("not_utf8.txt").write_bytes(bytes(bbj))14

You’ve created three files:rot13.txt,empty_file.txt, andnot_utf8.txt. The first file contains the letters that map to each other in theROT13 cipher. The second file is a completely empty file, while the third file contains some data that’s notUTF-8 encoded. As you’ll see soon, the last two files will create problems for your program.

To count the number of lines in two files, you provide both their names on the command line:

Shell
$pythoncount.pycount.pyrot13.txtcount.py             □□□□□□□□□□□□□□□□□□□□□□□□□□□□ (28)rot13.txt            □□□□□□□□□□□□□ (13)

You callcount_all_files() with all the arguments provided at the command line. The function then loops over each file name.

If you provide the name of a file that doesn’t exist, then your program will raise an exception that tells you so:

Shell
$pythoncount.pywrong_name.txtTraceback (most recent call last):  ...FileNotFoundError: [Errno 2] No such file or directory: 'wrong_name.txt'

Something similar will happen if you try to analyzeempty_file.txt ornot_utf8.txt:

Shell
$pythoncount.pyempty_file.txtTraceback (most recent call last):  ...UnboundLocalError: cannot access local variable 'line_num' where it is                   not associated with a value$pythoncount.pynot_utf8.txtTraceback (most recent call last):  ...UnicodeDecodeError: 'utf-8' codec can't decode byte 0xe5 in position 2:                    invalid continuation byte

Both cases raise errors. Forempty_file.txt, the issue is thatline_num gets defined by iterating over the lines of the file. If there are no lines in the file, thenline_num isn’t defined, and you get an error when you try to access it. The problem withnot_utf8.txt is that you try to UTF-8-decode something that isn’t UTF-8 encoded.

In the next subsections, you’ll use these errors to explore how exception groups can help you improve your error handling. For now, observe what happens if you try to analyze two files that both raise an error:

Shell
$pythoncount.pynot_utf8.txtempty_file.txtTraceback (most recent call last):  ...UnicodeDecodeError: 'utf-8' codec can't decode byte 0xe5 in position 2:                    invalid continuation byte

Note that only the first error, corresponding tonot_utf8.txt, is raised. This is natural, because the files are analyzed sequentially. That error happens long beforeempty_file.txt is opened.

Analyze Files Concurrently

In this subsection, you’ll rewrite your program to run asynchronously. This means that the analysis of all the files happens concurrently instead of sequentially. It’s instructive to see your updated program run:

Files being analyzed, all at the same time

The animation shows that lines are counted in all files at the same time, instead of in one file at the time like before.

You achieve this concurrency by rewriting your functions into asynchronous coroutines using theasync andawait keywords. Note that this new version still uses old async practices, and this code is runnable inPython 3.7 and later. In the next subsection, you’ll take the final step and use the new task groups.

Create a new file namedcount_gather.py with the following code:

Python
# count_gather.pyimportasyncioimportsysimportcoloramafromcoloramaimportCursorcolorama.init()asyncdefprint_at(row,text):print(Cursor.POS(1,1+row)+str(text))awaitasyncio.sleep(0.03)asyncdefcount_lines_in_file(file_num,file_name):counter_text=f"{file_name[:20]:<20} "withopen(file_name,mode="rt",encoding="utf-8")asfile:forline_num,_inenumerate(file,start=1):counter_text+="□"awaitprint_at(file_num,counter_text)awaitprint_at(file_num,f"{counter_text} ({line_num})")asyncdefcount_all_files(file_names):tasks=[asyncio.create_task(count_lines_in_file(file_num,file_name))forfile_num,file_nameinenumerate(file_names,start=1)]awaitasyncio.gather(*tasks)if__name__=="__main__":asyncio.run(count_all_files(sys.argv[1:]))

If you compare this code tocount.py from the previous subsection, then you’ll note that most changes only addasync to function definitions orawait to function calls. Theasync andawaitkeywords constitute Python’s API for doingasynchronous programming.

Note:asyncio is the library for doing asynchronous programming that’s included in Python’s standard library. However, Python’s asynchronous computing model is quite general, and you can use other third-party libraries likeTrio andCurio instead ofasyncio.

Alternatively, you can use third-party libraries likeuvloop andQuattro. These aren’t replacements forasyncio. Instead, they add performance or extra features on top of it.

Next, note thatcount_all_files() has changed significantly. Instead of sequentially callingcount_lines_in_file(), you create one task for each file name. Each task preparescount_lines_in_file() with the relevant arguments. All tasks are collected in a list and passed toasyncio.gather(). Finally,count_all_files() is initiated by callingasyncio.run().

What happens here is thatasyncio.run() creates anevent loop. The tasks are executed by the event loop. In the animation, it looks like all the files are analyzed at the same time. However, while the lines are counted concurrently, they’re not counted in parallel. There’s only one thread in your program, but the thread continously switches which task it’s working on.

Asynchronous programming is sometimes calledcooperative multitasking because each task voluntarily gives up control to let other tasks run. Think ofawait as a marker in your code where you decide that it’s okay to switch tasks. In the example, that’s mainly when the code sleeps before the next animation step.

Note:Threading achieves similar results but usespreemptive multitasking, where the operating system decides when to switch tasks. Asynchronous programming is typically easier to reason about than threading, because you know when tasks may take a break. SeeSpeed Up Your Python Program With Concurrency for a comparison of threading, asynchronous programming, and other kinds of concurrency.

Run your new code on a few different files and observe how they’re all analyzed in parallel:

Shell
$pythoncount_gather.pycount.pyrot13.txtcount_gather.pycount.py             □□□□□□□□□□□□□□□□□□□□□□□□□□□□ (28)rot13.txt            □□□□□□□□□□□□□ (13)count_gather.py      □□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□ (31)

As your files animate in your console, you’ll see thatrot13.txt finishes before the other tasks. Next, try to analyze a few of the troublesome files that you created earlier:

Shell
$pythoncount_gather.pynot_utf8.txtempty_file.txtTraceback (most recent call last):  ...UnicodeDecodeError: 'utf-8' codec can't decode byte 0xe5 in position 2:                    invalid continuation byte

Even thoughnot_utf8.txt andempty_file.txt are now analyzed concurrently, you only see the error raised for one of them. As you learned earlier, regular Python exceptions are handled one by one, andasyncio.gather() is limited by this.

Note: You can usereturn_exceptions=True as an argument when awaitingasyncio.gather(). This will collect exceptions from all your tasks and return them in a list when all tasks are finished. However, it’s complicated to then handle these exceptions properly, because they’re not using Python’s normal error handling.

Third-party libraries like Trio and Curio do some special error handling that’s able to deal with multiple exceptions. For example, Trio’sMultiError wraps two or more exceptions and provides a context manager that handles them.

More convenient handling of multiple errors is exactly one of the use cases that exception groups were designed to handle. In your counter application, you’d want to see a group containing one exception per file that fails to be analyzed, and have a simple way of handling them. It’s time to give the new Python 3.11TaskGroup a spin!

Control Concurrent Processing With Task Groups

Task groups have been a planned feature forasyncio for a long time. Yuri Selivanov mentions them as a possible enhancement forPython 3.8 inasyncio: What’s Next, a presentation he gave at PyBay 2018. Similar features have been available in other libraries, includingTrio’s nurseries,Curio’s task groups, andQuattro’s task groups.

The main reason the implementation has taken so much time is that task groups require properly dealing with several exceptions at once. The new exception group feature in Python 3.11 has paved the way for including asynchronous task groups as well. They were finallyimplemented byYury Selivanov andGuido van Rossum and made available in Python 3.11.0a6.

In this subsection, you’ll reimplement your counter application to useasyncio.TaskGroup instead ofasyncio.gather(). In the next subsection, you’ll useexcept* to conveniently handle the different exceptions that your application can raise.

Put the following code in a file namedcount_taskgroup.py:

Python
# count_taskgroup.pyimportasyncioimportsysimportcoloramafromcoloramaimportCursorcolorama.init()asyncdefprint_at(row,text):print(Cursor.POS(1,1+row)+str(text))awaitasyncio.sleep(0.03)asyncdefcount_lines_in_file(file_num,file_name):counter_text=f"{file_name[:20]:<20} "withopen(file_name,mode="rt",encoding="utf-8")asfile:forline_num,_inenumerate(file,start=1):counter_text+="□"awaitprint_at(file_num,counter_text)awaitprint_at(file_num,f"{counter_text} ({line_num})")asyncdefcount_all_files(file_names):asyncwithasyncio.TaskGroup()astg:forfile_num,file_nameinenumerate(file_names,start=1):tg.create_task(count_lines_in_file(file_num,file_name))if__name__=="__main__":asyncio.run(count_all_files(sys.argv[1:]))

Compare this tocount_gather.py. You’ll note that the only change is how tasks are created incount_all_files(). Here, you create the task group with a context manager. After that, your code is remarkably similar to the original synchronous implementation incount.py:

Python
defcount_all_files(file_names):forfile_num,file_nameinenumerate(file_names,start=1):count_lines_in_file(file_num,file_name)

Tasks that are created inside aTaskGroup are run concurrently, similar to tasks run byasyncio.gather(). Counting files should work identically to before, as long as you’re using Python 3.11:

Shell
$pythoncount_taskgroup.pycount.pyrot13.txtcount_taskgroup.pycount.py             □□□□□□□□□□□□□□□□□□□□□□□□□□□□ (28)rot13.txt            □□□□□□□□□□□□□ (13)count_taskgroup.py   □□□□□□□□□□□□□□□□□□□□□□□□□□□□□ (29)

One great improvement, though, is how errors are handled. Provoke your new code by analyzing some of your troublesome files:

Shell
$pythoncount_taskgroup.pynot_utf8.txtempty_file.txt  + Exception Group Traceback (most recent call last):  |   ...  | ExceptionGroup: unhandled errors in a TaskGroup (2 sub-exceptions)  +-+---------------- 1 ----------------    | Traceback (most recent call last):    |   File "count_taskgroup.py", line 18, in count_lines_in_file    |     for line_num, _ in enumerate(file, start=1):    |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^    | UnicodeDecodeError: 'utf-8' codec can't decode byte 0xe5 in position 2:    |                     invalid continuation byte    +---------------- 2 ----------------    | Traceback (most recent call last):    |   File "count_taskgroup.py", line 21, in count_lines_in_file    |     await print_at(file_num, f"{counter_text} ({line_num})")    |                                                 ^^^^^^^^    | UnboundLocalError: cannot access local variable 'line_num' where it is    |                    not associated with a value    +------------------------------------

Note that you get anException Group Traceback with two sub-exceptions, one for each file that fails to be analyzed. This is already an improvement overasyncio.gather(). In the next subsection, you’ll learn how you can handle these kinds of errors in your code.

Yuri Selivanov points out that the new task groups offer a better API than the oldasyncio.gather(), as task groups are “composable, predictable, and safe.” Additionally, he notes that task groups:

  1. Run a set of nested tasks. If one fails, all other tasks that are still running would be canceled.
  2. Allow to execute code (incl. awaits) between scheduling nested tasks.
  3. Thanks to ExceptionGroups, all errors are propagated and can be handled/reported.

Yuri Selivanov (Source)

In the next subsection, you’ll experiment with handling and reporting errors in your concurrent code.

Handle Concurrent Errors

You’ve written some concurrent code that sometimes raises errors. How can you handle those exceptions properly? You’ll see examples of error handling soon. First, though, you’ll add one more way that your code can fail.

The problems in your code that you’ve seen so far all raise before the analysis of the file begins. To simulate an error that may happen during the analysis, say that your tool suffers fromtriskaidekaphobia, meaning that it’s irrationally afraid of the number thirteen. Add two lines tocount_lines_in_file():

Python
# count_taskgroup.py# ...asyncdefcount_lines_in_file(file_num,file_name):counter_text=f"{file_name[:20]:<20} "withopen(file_name,mode="rt",encoding="utf-8")asfile:forline_num,_inenumerate(file,start=1):counter_text+="□"awaitprint_at(file_num,counter_text)awaitprint_at(file_num,f"{counter_text} ({line_num})")ifline_num==13:raiseRuntimeError("Files with thirteen lines are too scary!")# ...

If a file has exactly thirteen lines, then aRuntimeError is raised at the end of the analysis. You can see the effect of this by analyzingrot13.txt:

Shell
$pythoncount_taskgroup.pyrot13.txtrot13.txt            □□□□□□□□□□□□□ (13)  + Exception Group Traceback (most recent call last):  |   ...  | ExceptionGroup: unhandled errors in a TaskGroup (1 sub-exception)  +-+---------------- 1 ----------------    | Traceback (most recent call last):    |   File "count_taskgroup.py", line 23, in count_lines_in_file    |     raise RuntimeError("Files with thirteen lines are too scary!")    |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^    | RuntimeError: Files with thirteen lines are too scary!    +------------------------------------

As expected, your new triskaidekaphobic code balks at the thirteen lines inrot13.py. Next combine this with one of the errors you saw earlier:

Shell
$pythoncount_taskgroup.pyrot13.txtnot_utf8.txtrot13.txt            □  + Exception Group Traceback (most recent call last):  |   ...  | ExceptionGroup: unhandled errors in a TaskGroup (1 sub-exception)  +-+---------------- 1 ----------------    | Traceback (most recent call last):    |   File "count_taskgroup.py", line 18, in count_lines_in_file    |     for line_num, _ in enumerate(file, start=1):    |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^    | UnicodeDecodeError: 'utf-8' codec can't decode byte 0xe5 in position 2:    |                     invalid continuation byte    +------------------------------------

This time around, only one error is reported even though you know both files should raise an exception. The reason you get only one error is that the two issues would be raised at different times. One feature of task groups is that they implement acancel scope. Once some tasks fail, other tasks in the same task group are canceled by the event loop.

Note: Cancel scopes were pioneered byTrio. The final implementation of cancel scopes and which features they’ll support inasyncio is still being discussed. The following examples work in Python 3.11.0a7, but things may still change before Python 3.11 is finalized.

In general, there are two approaches that you can take to handle errors inside your asynchronous tasks:

  1. Use regulartryexcept blocks inside your coroutines to handle issues.
  2. Use the newtryexcept* blocks outside your task groups to handle issues.

In the first case, errors in one task will typically not affect other running tasks. In the second case, however, an error in one task will cancel all other running tasks.

Try this out for yourself! First, addsafe_count_lines_in_file() which uses regular exception handling inside your coroutines:

Python
# count_taskgroup.py# ...asyncdefsafe_count_lines_in_file(file_num,file_name):try:awaitcount_lines_in_file(file_num,file_name)exceptRuntimeErroraserr:awaitprint_at(file_num,err)asyncdefcount_all_files(file_names):asyncwithasyncio.TaskGroup()astg:forfile_num,file_nameinenumerate(file_names,start=1):tg.create_task(safe_count_lines_in_file(file_num,file_name))# ...

You also changecount_all_files() to call the newsafe_count_lines_in_file() instead ofcount_lines_in_file(). In this implementation, you only deal with theRuntimeError raised whenever a file has thirteen lines.

Note:safe_count_lines_in_file() doesn’t use any specific features of task groups. You could use a similar function to makecount.py andcount_gather.py more robust as well.

Analyzerot13.txt and some other files to confirm that the error no longer cancels the other tasks:

Shell
$pythoncount_taskgroup.pycount.pyrot13.txtcount_taskgroup.pycount.py             □□□□□□□□□□□□□□□□□□□□□□□□□□□□ (28)Files with thirteen lines are too scary!count_taskgroup.py   □□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□ (37)

Errors that are handled don’t bubble up and affect other tasks. In this example,count.py andcount_taskgroup.py were properly analyzed even though the analysis ofrot13.txt failed.

Next, try to useexcept* to handle errors after the fact. You can, for example, wrap your event loop in atryexcept* block:

Python
# count_taskgroup.py# ...if__name__=="__main__":try:asyncio.run(count_all_files(sys.argv[1:]))except*UnicodeDecodeErroraseg:print("Bad encoding:",*[str(e)[:50]foreineg.exceptions])

Recall thatexcept* works with exception groups. In this case, you loop through theUnicodeDecodeError exceptions in the group and print their first fifty characters to the console to log them.

Analyzenot_utf8.txt together with some other files to see the effect:

Shell
$pythoncount_taskgroup.pyrot13.txtnot_utf8.txtcount.pyrot13.txt            □count.py             □Bad encoding: 'utf-8' codec can't decode byte 0xe5 in position 2

In contrast to the previous example, the other tasks are canceled even though you handle theUnicodeDecodeError. Note that only one line is counted in bothrot13.txt andcount.py.

Note: You can wrap the call tocount_all_files() inside a regulartryexcept block in thecount.py andcount_gather.py examples. However, this will only allow you to deal with at most one error. In contrast, task groups can report all errors:

Shell
$pythoncount_taskgroup.pynot_utf8.txtcount_taskgroup.pyempty.txtcount_taskgroup.py   □Bad text: ["'utf-8' codec can't decode byte 0xe5 in position 2"]Empty file: ["cannot access local variable 'line_num' where it i"]

This example shows the result of having several concurrent errors after you expand the code in the previous example to deal with bothUnicodeDecodeError andUnboundLocalError.

If you don’t handle all exceptions that are raised, then the unhandled exceptions will still cause your program to crash with a traceback. To see this, switchcount.py toempty_file.txt in your analysis:

Shell
$pythoncount_taskgroup.pyrot13.txtnot_utf8.txtempty_file.txtrot13.txt            □Bad encoding: 'utf-8' codec can't decode byte 0xe5 in position 2  + Exception Group Traceback (most recent call last):  |   ...  | ExceptionGroup: unhandled errors in a TaskGroup (1 sub-exception)  +-+---------------- 1 ----------------    | Traceback (most recent call last):    |   File "count_taskgroup.py", line 21, in count_lines_in_file    |     await print_at(file_num, f"{counter_text} ({line_num})")    |                                                 ^^^^^^^^    | UnboundLocalError: cannot access local variable 'line_num' where it is    |                    not associated with a value    +------------------------------------

You get the familiarUnboundLocalError. Note that part of the error message points out that there’s one unhandled sub-exception. There’s no record in the traceback of theUnicodeDecodeError sub-exception that you did handle.

You’ve now seen an example of using task groups in order to improve the error handling of your asynchronous application, and in particular being able to comfortably handle several errors happening at the same time. The combination of exception groups and task groups makes Python a very capable language for doing asynchronous programming.

Other New Features

In every new version of Python, a handful of features get most of the buzz. However, most of the evolution of Python has happened in small steps, by adding a function here or there, improving some existing functionality, or fixing a long-standing bug.

Python 3.11 is no different. This section shows a few of the smaller improvements waiting for you in Python 3.11.

Annotate Exceptions With Custom Notes

You can now add custom notes to an exception. This is yet another improvement to how exceptions are handled in Python. Exception notes were suggested byZac Hatfield-Dodds inPEP 678: Enriching Exceptions with Notes. The PEP has beenaccepted, and an early version of the proposal was implemented for Python 3.11.0a3 to Python 3.11.0a7.

In those alpha versions, you can assign strings to a.__note__ attribute on an exception, and that information will be made available if the error isn’t handled. Here’s a basic example:

Python
>>>try:...raiseValueError(678)...exceptValueErroraserr:...err.__note__="Enriching Exceptions with Notes"...raise...Traceback (most recent call last):...ValueError:678Enriching Exceptions with Notes

You’re adding a note to theValueError before reraising it. Your note is then displayed together with the regular error message at the end of your traceback.

Note: The rest of this section was updated on May 9, 2022 to reflect changes to the exception notes feature that was made available with the release ofPython 3.11.0b1.

Duringdiscussions of the PEP.__note__ was changed to.__notes__ which can contain several notes. A list of notes can be useful in certain use cases where keeping track of individual notes is important. One example of this is internationalization andtranslation of notes.

There is a also new dedicated method,.add_note(), that can be used to add these notes. Thefull implementation of PEP 678 is available in thefirst beta version of Python 3.11 and later.

Going forward, you should write the previous example as follows:

Python
>>>try:...raiseValueError(678)...exceptValueErroraserr:...err.add_note("Enriching Exceptions with Notes")...raise...Traceback (most recent call last):...ValueError:678Enriching Exceptions with Notes

You can add several notes with repeated calls to.add_note() and recover them by looping over.__notes__. All notes will be printed below the traceback when the exception is raised:

Python
>>>err=ValueError(678)>>>err.add_note("Enriching Exceptions with Notes")>>>err.add_note("Python 3.11")>>>err.__notes__['Enriching Exceptions with Notes', 'Python 3.11']>>>fornoteinerr.__notes__:...print(note)...Enriching Exceptions with NotesPython 3.11>>>raiseerrTraceback (most recent call last):...ValueError:678Enriching Exceptions with NotesPython 3.11

The new exception notes are also compatible with the exception groups.

Reference the Active Exception Withsys.exception()

Internally, Python has represented an exception as a tuple with information about the type of the exception, the exception itself, and the traceback of the exception. Thischanges in Python 3.11. Now, Python will internally store only the exception itself. Both the type and the traceback can be derived from the exception object.

In general, you won’t need to think about this change, as it’s all under the hood. However, if you need to access an active exception, you can now use the newexception() function in thesys module:

Python
>>>importsys>>>try:...raiseValueError("bpo-46328")...exceptValueError:...print(f"Handling{sys.exception()}")...Handling bpo-46328

Note that you usually won’t useexception() in normal error handling like above. Instead, it’s sometimes handy to use in wrapper libraries that are used in error handling but don’t have direct access to active exceptions. In normal error handling, you should name your errors in theexcept clause:

Python
>>>try:...raiseValueError("bpo-46328")...exceptValueErroraserr:...print(f"Handling{err}")...Handling bpo-46328

In versions prior to Python 3.11, you can get the same information fromsys.exc_info():

Python
>>>try:...raiseValueError("bpo-46328")...exceptValueError:...sys.exception()issys.exc_info()[1]...True

Indeed,sys.exception() is identical tosys.exc_info()[1]. The new function was added inbpo-46328 by Irit Katriel, although the idea was originally floated inPEP 3134, all the way back in 2005.

Reference the Active Traceback Consistently

As noted in the previous subsection, older versions of Python represent exceptions as tuples. You can access traceback information in two different ways:

Python
>>>importsys>>>try:...raiseValueError("bpo-45711")...exceptValueError:...exc_type,exc_value,exc_tb=sys.exc_info()...exc_value.__traceback__isexc_tb...True

Note that accessing the traceback throughexc_value andexc_tb returns the exact same object. In general, this is what you want. However, it turns out that there has been a subtle bug hiding around for some time. You can update the traceback onexc_value without updatingexc_tb.

To demonstrate this, code up the following program, which changes the traceback during handling of an exception:

Python
 1# traceback_demo.py 2 3importsys 4importtraceback 5 6deftb_last(tb): 7frame,*_=traceback.extract_tb(tb,limit=1) 8returnf"{frame.name}:{frame.lineno}" 910defbad_calculation():11return1/01213defmain():14try:15bad_calculation()16exceptZeroDivisionErroraserr:17err_tb=err.__traceback__18err=err.with_traceback(err_tb.tb_next)1920exc_type,exc_value,exc_tb=sys.exc_info()21print(f"{tb_last(exc_value.__traceback__)= }")22print(f"{tb_last(exc_tb)= }")2324if__name__=="__main__":25main()

You change the traceback of the active exception on line 18. As you’ll soon see, this wouldn’t update the traceback part of the exception tuple in Python 3.10 and earlier. To show this, lines 20 to 22 compare the last frame of the tracebacks referenced by the active exception and the traceback object.

Run this with Python 3.10 or an earlier version:

Shell
$pythontraceback_demo.pytb_last(exc_value.__traceback__) = 'bad_calculation:11'tb_last(exc_tb)                  = 'main:15'

The important thing to note here is that the two line references are different. The active exception points to the updated location, line 11 insidebad_calculation(), while the traceback points to the old location insidemain().

In Python 3.11, the traceback part of the exception tuple is always read from the exception itself. Therefore, the inconsistency is gone:

Shell
$python3.11traceback_demo.pytb_last(exc_value.__traceback__) = 'bad_calculation:11'tb_last(exc_tb)                  = 'bad_calculation:11'

Now, both ways of accessing the traceback give the same result. This fixes a bug that has been present in Python for some time. Still, it’s important to note that the inconsistency was mostly academic. Yes, the old way was wrong, but it’s unlikely that it caused issues in actual code.

This bug fix is interesting because it lifts the curtain on something bigger. As you learned in the previous subsection, Python’s internal representation of exceptions changes in version 3.11. This bug fix is an immediate consequence of that change.

Restructuring Python’s exceptions is part of an evenbigger effort to optimize many different parts of Python.Mark Shannon has initiated thefaster-cpython project. Streamlining exceptions is only one of the ideas coming out of that initiative.

The smaller improvements that you’ve learned about in this section examplify all the work that goes into maintaining and developing a programming language, beyond the fewitems stealing most of the headlines. The features that you’ve learned about here are all related to Python’s exception handling. However, there are many other small changes happening as well.What’s New In Python 3.11 keeps track of all of them.

Conclusion

In this tutorial, you’ve learned about some of the new capabilities that Python 3.11 will bring to the table when it’s released in October 2022. You’ve seen some of its new features and explored how you can already play with the improvements.

In particular, you’ve:

  • Installed an alpha version of Python 3.11 on your computer
  • Exploredexception groups and how you use them to organize errors
  • Usedexcept* tofilter exception groups andhandle different types of errors
  • Rewritten yourasynchronous code to usetask groups to initiate concurrent workflows
  • Tried out a few of the smaller improvements in Python 3.11, includingexception notes and a new internalrepresentation of exceptions

Try out task and exception groups in Python 3.11! Do you have a use case for them? Comment below to share your experience.

Free Download:Click here to download free sample code that demonstrates some of the new features of Python 3.11.

🐍 Python Tricks 💌

Get a short & sweetPython Trick delivered to your inbox every couple of days. No spam ever. Unsubscribe any time. Curated by the Real Python team.

Python Tricks Dictionary Merge

AboutGeir Arne Hjelle

Geir Arne is an avid Pythonista and a member of the Real Python tutorial team.

» More about Geir Arne

Each tutorial at Real Python is created by a team of developers so that it meets our high quality standards. The team members who worked on this tutorial are:

MasterReal-World Python Skills With Unlimited Access to Real Python

Locked learning resources

Join us and get access to thousands of tutorials, hands-on video courses, and a community of expert Pythonistas:

Level Up Your Python Skills »

MasterReal-World Python Skills
With Unlimited Access to Real Python

Locked learning resources

Join us and get access to thousands of tutorials, hands-on video courses, and a community of expert Pythonistas:

Level Up Your Python Skills »

What Do You Think?

Rate this article:

What’s your #1 takeaway or favorite thing you learned? How are you going to put your newfound skills to use? Leave a comment below and let us know.

Commenting Tips: The most useful comments are those written with the goal of learning from or helping out other students.Get tips for asking good questions andget answers to common questions in our support portal.


Looking for a real-time conversation? Visit theReal Python Community Chat or join the next“Office Hours” Live Q&A Session. Happy Pythoning!

Keep Learning

Related Topics:advancedpython

Related Tutorials:

Keep reading Real Python by creating a free account or signing in:

Already have an account?Sign-In

Almost there! Complete this form and click the button below to gain instant access:

Python 3.11: Cool New Features for You to Try

Python 3.11 Demos (Sample Code)

🔒 No spam. We take your privacy seriously.


[8]ページ先頭

©2009-2025 Movatter.jp