Search code examples
pythonpython-3.xvariadic-functions

How do I override the `**` operator used for kwargs in variadic functions for my own user-defined classes?


I would like to be able to unpack my own dictionary-like class.

class FauxDict:
    def __getitem__(self, key):
        return 99
    def __iter__(self):
        return range(0, 1)
    def to_map(self):
        return map(lambda x: True, range(0, 2))

def bar(**kwargs):
    pass

dct = {"x":1, "y":2}
bar(**dct) # no error

dct = FauxDict()
bar(**dct) # error

dct = FauxDict()
bar(**dct.to_map()) # error

The errors are:

bar(**dct) # error
TypeError: bar() argument after ** must be a mapping, not FauxDict

bar(**dct.to_map()) # error
TypeError: bar() argument after ** must be a mapping, not map

Also, which python class(es) technically qualify as being mappings?


Solution

  • Implementing .keys() and .__getitem__() will be sufficient to allow an instance of your custom class to be expanded using **.

    The relevant parts of the cpython source are in ceval.c which uses _PyDict_MergeEx, and thus dict_merge from dictobject.c which states:

        /* We accept for the argument either a concrete dictionary object,
         * or an abstract "mapping" object.  For the former, we can do
         * things quite efficiently.  For the latter, we only require that
         * PyMapping_Keys() and PyObject_GetItem() be supported.
         */
    

    And indeed, implementing these two methods works as you would expect:

    class MyMapping:
        def __init__(self, d):
            self._d = d
    
        def __getitem__(self, k):
            return self._d[k]
    
        def keys(self):
            return self._d.keys()
    
    
    def foo(a, b):
        print(f"a: {a}")
        print(f"b: {b}")
    
    mm = MyMapping({"a":"A", "b":"B"})
    foo(**mm)
    

    Output:

    a: A
    b: B
    

    Side note: your .keys() implementation need only return an iterable (e.g. a list would be fine), not necessarily a dict_keys object like I do above for simplicity. That line could also have been return list(self._d.keys()) without issue.

    Something unusual like the following would also work:

    class MyMapping:
        def __getitem__(self, k):
            return 2
    
        def keys(self):
            return ["a", "b", "c"]
    
    def foo(a, b, **kwargs):
        print(f"a: {a}")
        print(f"b: {b}")
        print(f"kwargs: {kwargs}")
    
    mm = MyMapping()
    foo(**mm)
    

    Output:

    a: 2
    b: 2
    kwargs: {'c': 2}