Why does handling multiple exceptions require a tuple and not a list?
The error handling, written in C, uses type checking for the special case of a tuple, before other type-checking and exception handling, so that multiple types of exceptions may be caught.
At least one Python core developer advocates using exception handling for control flow. Adding lists as an additional type to check would work against this strategy.
It appears that expanding this to allow for sets or lists has not been specifically addressed by the core development team, though I will gladly reference it if it can be found. There has been a discussion on the Python mailing list that speculates quite a lot (another answer here quotes one response at length).
After performing the below analysis, and in the context of the mailing list discussion, I think the reasoning is obvious. I do not suggest proposing to add other containers.
Demonstration that lists fail versus tuples
exceptions = TypeError, RuntimeError
list_of_exceptions = list(exceptions)
Catching the tuple of exceptions does work:
try:
raise TypeError('foo')
except exceptions as error:
print(error)
outputs:
foo
But catching the list of exceptions does not work:
try:
raise TypeError('foo')
except list_of_exceptions as error:
print(error)
prints:
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
TypeError: foo
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "<stdin>", line 3, in <module>
TypeError: catching classes that do not inherit from BaseException is not allowed
This demonstrates that we are doing type checking for the special case of a tuple. It would certainly make the code slower to add another type to check for, and the core developers have been saying that it's a good thing to use exception handling for control flow in Python for a while now.
Source Code analysis
An analysis of the source agrees with this above conclusion.
Grammar
This is not an issue for Python's grammar or parsing. It will accept any expression. Thus any expression that results in an Exception or tuple of Exceptions should be legal.
Disassembly
If we disassemble a function that does this in Python 3, we see that it looks to match an exception with a comparison operation.
def catch(exceptions):
try:
raise Exception
except exceptions:
pass
import dis
dis.dis(catch)
Which outputs:
2 0 SETUP_EXCEPT 10 (to 13)
3 3 LOAD_GLOBAL 0 (Exception)
6 RAISE_VARARGS 1
9 POP_BLOCK
10 JUMP_FORWARD 18 (to 31)
4 >> 13 DUP_TOP
14 LOAD_FAST 0 (exceptions)
17 COMPARE_OP 10 (exception match)
...
This leads us inside the Python interpreter.
Internal control flow - CPython's implementation details
The CPython control flow first checks for if the value is a tuple. If so,
it iterates through the tuple using tuple specific code - looking for the value to be a Exception:
case PyCmp_EXC_MATCH:
if (PyTuple_Check(w)) {
Py_ssize_t i, length;
length = PyTuple_Size(w);
for (i = 0; i < length; i += 1) {
PyObject *exc = PyTuple_GET_ITEM(w, i);
if (!PyExceptionClass_Check(exc)) {
_PyErr_SetString(tstate, PyExc_TypeError,
CANNOT_CATCH_MSG);
return NULL;
}
}
}
else {
if (!PyExceptionClass_Check(w)) {
_PyErr_SetString(tstate, PyExc_TypeError,
CANNOT_CATCH_MSG);
return NULL;
}
}
res = PyErr_GivenExceptionMatches(v, w);
break;
Adding another type would require more internal control flow, slowing down control flow inside of the Python interpreter.
Sizes of Python Containers
Tuples are lightweight arrays of pointers. So are lists, but they may be allocated extra space so that you may quickly add to them (up to the point they need to get larger). In Python 3.7.3 on Linux:
>>> from sys import getsizeof
>>> getsizeof((1,2,3))
72
>>> getsizeof([1,2,3])
88
Sets take up even more space because they are hash tables. They have both a hash of the object they contain as well as a pointer to that which they point to.
Conclusion
This is for the CPython core development team to debate and decide.
But my conclusion is that slowing down control flow in Python by checking for other types even at the C level would work against the strategy of using exception handling for control flow in Python modules.
After reasoning through the above, I would not propose that they add this.