Skip to content

dict and dict.update treat the first argument as a mapping when it has attribute keys without attribute __getitem__ #116938

@Prometheus3375

Description

@Prometheus3375

Bug report

How to reproduce

  1. Make such a class:
    class Object:
        def __iter__(self, /):
            return iter(())
    
        def keys(self, /):
            return ['1', '2']
  2. Call dict(Object()).
  3. Call d = {} and then d.update(Object()).

Expected result

At the step 2 an empty dictionary is returned.
At the step 3 d stays empty.

Actual result

Both steps 2 and 3 raise a TypeError 'Object' object is not subscriptable.

CPython versions tested on:

3.10
3.11

Operating systems tested on:

Windows 21H2 (19044.1645)
Ubuntu 20.04.6 LTS


Docs of dict state:

If a positional argument is given and it is a mapping object, a dictionary is created with the same key-value pairs as the mapping object. Otherwise, the positional argument must be an iterable object.

Unfortunately, there is no link to what is considered as a mapping object.
In typeshed both dict and dict.update accept SupportsKeysAndGetItem, i.e., any object with attributes keys and __getitem__.

But the experiment above shows that only keys is enough. While typeshed is a bit too restrictive in the case for iterable (only iterables of 2-sized tuples are allowed, but dict accepts any iterable of 2-sized iterables), I think just checking for keys is not enough.

In the actual C code there is such comment:

/* PyDict_Merge updates/merges from a mapping object (an object that
supports PyMapping_Keys() and PyObject_GetItem()). If override is true,
the last occurrence of a key wins, else the first. The Python
dict.update(other) is equivalent to PyDict_Merge(dict, other, 1).
*/

Thus, it is intended to check the presence of two attributes.


The error is here:

cpython/Objects/dictobject.c

Lines 3426 to 3441 in 2982bdb

/* Single-arg dict update; used by dict_update_common and operators. */
static int
dict_update_arg(PyObject *self, PyObject *arg)
{
if (PyDict_CheckExact(arg)) {
return PyDict_Merge(self, arg, 1);
}
int has_keys = PyObject_HasAttrWithError(arg, &_Py_ID(keys));
if (has_keys < 0) {
return -1;
}
if (has_keys) {
return PyDict_Merge(self, arg, 1);
}
return PyDict_MergeFromSeq2(self, arg, 1);
}

This code evaluates whether attribute keys is present. If the answer is true, calls PyDict_Merge, and calls PyDict_MergeFromSeq2 otherwise.

Linked PRs

Metadata

Metadata

Assignees

No one assigned

    Labels

    docsDocumentation in the Doc dirinterpreter-core(Objects, Python, Grammar, and Parser dirs)type-bugAn unexpected behavior, bug, or error

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions