Passing keyword arguments to custom exceptions - anomaly

1.8k views Asked by At

Why am I getting different results for the following two code snippets (Python 3.4):

class MainError(Exception):
    def __init__(self, msg, **parms):
        super().__init__()  
        self.msg = msg
        self.parms = parms
        print('Parms original', parms)

    def __str__(self):
        return self.msg + ':' + str(self.parms)

class SubError(MainError):
    def __init__(self, msg, **parms):
        super().__init__(msg, **parms)

try:
    raise SubError('Error occured', line = 22, col = 11)
except MainError as e:
    print(e)


>>> 
Parms original {'line': 22, 'col': 11}
Error occured:{'line': 22, 'col': 11}

And:

class MainError(Exception):
    def __init__(self, msg, **args):
        super().__init__()  
        self.msg = msg
        self.args = args
        print('Parms original', args)

    def __str__(self):
        return self.msg + ':' + str(self.args)

class SubError(MainError):
    def __init__(self, msg, **args):
        super().__init__(msg, **args)

try:
    raise SubError('Error occured', line = 22, col = 11)
except MainError as e:
    print(e)


>>> 
Parms original {'line': 22, 'col': 11}
Error occured:('line', 'col')
1

There are 1 answers

0
Alex Huszagh On BEST ANSWER

It's because the error args are overwritten, by converting them to a Python tuple at the C level.

Here is the BaseException class for Python: https://hg.python.org/cpython/file/tip/Objects/exceptions.c

Starting at line 31, we see the following:

static PyObject *
BaseException_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
{
    PyBaseExceptionObject *self;

    self = (PyBaseExceptionObject *)type->tp_alloc(type, 0);
    if (!self)
        return NULL;
    /* the dict is created on the fly in PyObject_GenericSetAttr */
    self->dict = NULL;
    self->traceback = self->cause = self->context = NULL;
    self->suppress_context = 0;

    if (args) {
        self->args = args;
        Py_INCREF(args);
        return (PyObject *)self;
    }

    self->args = PyTuple_New(0);
    if (!self->args) {
        Py_DECREF(self);
        return NULL;
    }

    return (PyObject *)self;
}

Likewise, the init call has the same tuple conversion:

BaseException_init(PyBaseExceptionObject *self, PyObject *args, PyObject *kwds)
{
    PyObject *tmp;

    if (!_PyArg_NoKeywords(Py_TYPE(self)->tp_name, kwds))
        return -1;

    tmp = self->args;
    self->args = args;
    Py_INCREF(self->args);
    Py_XDECREF(tmp);

    return 0;
}

In short, self.args is getting converted to a tuple which is converted back to a string, which causes the difference.

The BaseException class is called for (I believe) all the method wrappers as a required argument.

This is appreciable if pass it a non-iterable argument (such as an integer):

>>> class CustomException(Exception):
...     def __init__(self):
...         super(CustomException, self).__init__('a')
...         self.args = 1
...     def __repr__(self):
...         print(self.args)
...         return ''
... 
>>> CustomException()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 4, in __init__
TypeError: 'int' object is not iterable

Moral of the story: Don't name your variables words that are constantly redefined and are key terms for the class you are using.