Search code examples
pythondefaultdict

Construct defaultdict by passing default_factory as named parameter


I get some strange behaviour of colletions.defaultdict:

import collections

c1 = collections.defaultdict(str)
c1['new']  # Works!

c2 = collections.defaultdict(default_factory=str)
c2['new']  # Raises KeyError... 

Why raises c2 a KeyError?

Sometimes I like naming parameter because I think it increases readability.

First I thought maybe python does not allow me to pass the parameter by naming it and puts my default_factory parameter to the kwargs, so I checked:

def func(first, **kwargs):
    print(first)
    print(kwargs)

func(first='one', second='two')

This outputs:

one
{'second': 'two'}

So this is not the case.


Solution

  • The default_factory parameter of the defaultdict constructor is positional-only and doesn't really have a name. If you try to pass it by name, you are just passing a completely unrelated keyword argument. Since keyword arguments to the defaultdict constructor are interpreted as its initial contents, your dict starts out having a single key "default_factory" whose value is the str type object.

    To understand how this works, imagine a function like this:

    def func(*args, **kwds):
        (default_factory,) = args
        for k, v in kwds.items():
            print(k, v)  # do something with keys and values
    

    If the documentation of this function were to name the positional argument default_factory, that might be a correct description of its meaning, but it would be misleading if it implied that one could pass it as a keyword argument.

    Some built-in functions are like that because it is very easy to define positional-only arguments in CPython C code. With defaultdict, it is by design, to allow literally any string key to be used as the part of initial content, without having an exception for a key that happens to be named default_factory.