BaseException.__str__ is defined here
static PyObject *
BaseException_str(PyBaseExceptionObject *self)
{
switch (PyTuple_GET_SIZE(self->args)) {
case 0:
return PyUnicode_FromString("");
case 1:
return PyObject_Str(PyTuple_GET_ITEM(self->args, 0));
default:
return PyObject_Str(self->args);
}
}
roughly translated into pseudocode:
def __str__(self) -> str:
if len(self.args) == 0:
return ''
elif len(self.args) == 1:
return str(self.args[0])
else:
return str(self.args)
so in this case, even though you initialize Exception.__init__ with a doubling of msg, you immediately clobber self.args with the original value
if you print(self.args) before you assign it, you'll see your doubled message
if you remove your self.args assignment you'll get the value you expect
.argstoself.args = (msg,)If you remove that line, it printsexexsuper().__init__(msg + msg)without messing with.argsis better pattern.