3

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 Answer 1

2

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.

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.