5

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.

marsl
  • 959
  • 1
  • 14
  • 25

1 Answers1

9

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.

user4815162342
  • 141,790
  • 18
  • 296
  • 355
  • I think it might help to add that defaultdict is a built-in class and is not defined in Python, hence it can have a positional-only argument. – Alex Hall Apr 27 '18 at 18:38
  • @AlexHall Your original comment had a point, so I amended the answer. I don't think it's necessary to stress `defaultdict` being built-in because the same effect can be achieved in pure Python (which the answer now demonstrates). – user4815162342 Apr 27 '18 at 18:41
  • 1
    Cool, I didn't realise you could make positional-only arguments in Python. Also nice tuple unpacking trick to get exactly one argument. – Alex Hall Apr 27 '18 at 18:45
  • Thank you very much, you helped me getting something fundamental about python in my head. This was driving me nuts. Probably, the [documentation](https://docs.python.org/3.6/library/collections.html#collections.defaultdict) is a misleading here. But no, just read more carefully :-) – marsl Apr 27 '18 at 19:04
  • @marsl Yes, the `func([arg])` syntax is a hint (albeit subtle) that `arg` is a positional parameter which is optional, rather than the standard Python's either-positional-or-keyword parameter. – user4815162342 Apr 27 '18 at 19:09
  • Is there any what someone could know this from the documentation? – user48956 Aug 07 '22 at 20:55
  • 1
    @user48956 The [documentation for Python 3.9+](https://docs.python.org/3.9/library/collections.html#collections.defaultdict) says `defaultdict(default_factory=None, /[, ...])`, meaning that `default_factory` is a positional-only argument (see https://stackoverflow.com/questions/24735311/what-does-the-slash-mean-in-help-output). – mkrieger1 Jan 22 '23 at 01:30