Search code examples
pythonlanguage-design

Why don't python dict keys/values quack like a duck?


Python is duck typed, and generally this avoids casting faff when dealing with primitive objects.

The canonical example (and the reason behind the name) is the duck test: If it looks like a duck, swims like a duck, and quacks like a duck, then it probably is a duck.

However one notable exception is dict keys/values, which look like a duck and swim like a duck, but notably do not quack like a duck.

>>> ls = ['hello']
>>> d = {'foo': 'bar'}
>>> for key in d.keys():
..      print(key)
..
'foo'
>>> ls + d.keys()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: can only concatenate list (not "dict_keys") to list

Can someone enlighten me as to why this is?


Solution

  • There is an explicit check for list type (or its children) in python source code (so even tuple doesn't qualify):

    static PyObject *
    list_concat(PyListObject *a, PyObject *bb)
    {
        Py_ssize_t size;
        Py_ssize_t i;
        PyObject **src, **dest;
        PyListObject *np;
        if (!PyList_Check(bb)) {
            PyErr_Format(PyExc_TypeError,
                      "can only concatenate list (not \"%.200s\") to list",
                      bb->ob_type->tp_name);
            return NULL;
        }
    

    so python can compute size very quickly and reallocate the result without trying all containers or iterate on the right hand to find out, providing very fast list addition.

    #define b ((PyListObject *)bb)
        size = Py_SIZE(a) + Py_SIZE(b);
        if (size < 0)
            return PyErr_NoMemory();
        np = (PyListObject *) PyList_New(size);
        if (np == NULL) {
            return NULL;
        }
    

    One way to workaround this is to use in-place extension/addition:

    my_list += my_dict  # adding .keys() is useless
    

    because in that case, in-place add iterates on the right hand side: so every collection qualifies.

    (or of course force iteration of the right hand: + list(my_dict))

    So it could accept any type but I suspect that the makers of python didn't find it worth it and were satisfied with a simple & fast implementation which is used 99% of the time.