Alvaro Alvaro - 6 months ago 14
Python Question

Why no __getitem__ raises TypeError

So the question is pretty simple:
If we have a random class, let's say an int and we try to access a non defined attribute:

my_int = 5
my_int.this_is_a_test


We will get this error:

AttributeError: 'int' object has no attribute 'this_is_a_test'


But if we try to access an index of it (in which case Python will do a lookup for a
__getitem__
attribute):

my_int = 5
my_int[0]


We get:

TypeError: 'int' object has no attribute '__getitem__'


What is the logic behind the change in exception type? It seems weird to me that a
TypeError
is raised, complaining about a missing attribute (
AttributeError
seems like a much better candidate for that)

Answer

It depends on your intention.

In [1]: my_int = 5

In [2]: my_int.__getitem__(0)  # -> AttributeError

In [3]: my_int[0]  # -> TypeError

When you use . you implicitly call the getattr function, that naturally raises the AttributeError if the attribute doesn't exist.

Update 2. Let's look at the bytecode.

In [11]: import dis

In [12]: def via_operator():
             my_int = 5
             my_int[0]


In [13]: def via_getattr():
             my_int = 5
             my_int.__getitem__(0)

In [14]: dis.dis(via_operator)
  2           0 LOAD_CONST               1 (5)
              3 STORE_FAST               0 (my_int)

  3           6 LOAD_FAST                0 (my_int)
              9 LOAD_CONST               2 (0)
             12 BINARY_SUBSCR       
             13 POP_TOP             
             14 LOAD_CONST               0 (None)
             17 RETURN_VALUE        

In [15]: dis.dis(via_getattr)
  2           0 LOAD_CONST               1 (5)
              3 STORE_FAST               0 (my_int)

  3           6 LOAD_FAST                0 (my_int)
              9 LOAD_ATTR                0 (__getitem__)
             12 LOAD_CONST               2 (0)
             15 CALL_FUNCTION            1
             18 POP_TOP             
             19 LOAD_CONST               0 (None)
             22 RETURN_VALUE   

As you see, the [] has a special virtual-machine instruction. From the docs

BINARY_SUBSCR: Implements TOS = TOS1[TOS].

Hence it's quite natural to raise a TypeError, when you fail at executing an instruction.

Update 1: Looking at the getattr sources, it's clear that this function can never raise such a TypeError, hence the [] operator doesn't call it under the hood (for the built-in types at least, though it's better to find the sources to clarify this bit).

static PyObject *
builtin_getattr(PyObject *self, PyObject *args)
{
    PyObject *v, *result, *dflt = NULL;
    PyObject *name;

    if (!PyArg_UnpackTuple(args, "getattr", 2, 3, &v, &name, &dflt))
        return NULL;
#ifdef Py_USING_UNICODE
    if (PyUnicode_Check(name)) {
        name = _PyUnicode_AsDefaultEncodedString(name, NULL);
        if (name == NULL)
            return NULL;
    }
#endif

    if (!PyString_Check(name)) {
        PyErr_SetString(PyExc_TypeError,
                        "getattr(): attribute name must be string");
        return NULL;
    }
    result = PyObject_GetAttr(v, name);
    if (result == NULL && dflt != NULL &&
        PyErr_ExceptionMatches(PyExc_AttributeError))
    {
        PyErr_Clear();
        Py_INCREF(dflt);
        result = dflt;
    }
    return result;
}