Back to Blog
Python

Python dict vs defaultdict: Choosing the Right Mapping

python dict vs defaultdict: Compare Python's dict and defaultdict, understand missing-key behavior, and decide which mapping fits your data-processing needs.

dictdefaultdictPython collectionsmissing keysdata structuresPython mapping
Illustration comparing a plain Python dict and a defaultdict with a default factory for missing keys.

When a key is missing from a regular dict, Python raises a KeyError unless you handle it explicitly. A defaultdict from the collections module changes that behavior by calling a factory function for missing keys. That single difference drives most of the decision between python dict vs defaultdict in real code.

What Happens on Missing Keys

A plain dict has no built-in behavior for a missing key. Accessing d["missing"] raises KeyError, and using d.get("missing") returns None by default. You must decide how to handle the absence at each call site.

config = {"host": "localhost"} # Raises KeyError # port = config["port"] # Returns None port = config.get("port")

A defaultdict stores a factory function that is called whenever a key is not present. The returned value is inserted into the dictionary and then returned.

from collections import defaultdict counts = defaultdict(int) counts["apples"] += 1 print(counts) # defaultdict(<class 'int'>, {'apples': 1})

Here int() returns 0, so the first increment works without an explicit check. The key "apples" is added to the dictionary with the value 0, then incremented to 1.

When to Use a Plain dict

A regular dict is the natural choice when you want explicit control over missing keys. If your code treats a missing key as an error, or if you need to distinguish between a key that is absent and a key that has a None value, a plain dict gives you that distinction.

user_preferences = {} if "theme" in user_preferences: theme = user_preferences["theme"] else: theme = "light"

Using in checks or get() with a default value keeps the behavior explicit. This is often preferable in APIs where you want to validate that required keys exist rather than silently filling in defaults.

A plain dict also works well when the set of keys is small and known ahead of time. For example, a configuration object with a fixed set of attributes rarely needs defaultdict; you can use get() with sensible defaults or raise an error for invalid input.

When defaultdict Fits

defaultdict shines when you are building a mapping from data that may contain repeated keys. Grouping items, counting occurrences, and accumulating lists are common patterns where the default factory removes boilerplate.

from collections import defaultdict words = ["apple", "banana", "apple", "cherry", "banana"] word_counts = defaultdict(int) for word in words: word_counts[word] += 1 print(dict(word_counts)) # {'apple': 2, 'banana': 2, 'cherry': 1}

Without defaultdict, you would need to check whether each key exists before incrementing:

word_counts = {} for word in words: if word in word_counts: word_counts[word] += 1 else: word_counts[word] = 1

The defaultdict version is shorter and prevents the key from being looked up twice. It also works well for building lists of related values:

from collections import defaultdict categories = defaultdict(list) categories["fruit"].append("apple") categories["fruit"].append("banana") categories["vegetable"].append("carrot") print(dict(categories)) # {'fruit': ['apple', 'banana'], 'vegetable': ['carrot']} ```n``` ## Default Factory and Its Pitfalls The factory argument to `defaultdict` must be a callable that takes no arguments. Common choices are `int`, `list`, `set`, `dict`, or a lambda like `lambda: "default"`. The factory is called only when a key is missing, and its return value is stored in the dictionary. A frequent mistake is passing a value instead of a callable: ```python # Wrong: this raises TypeError # d = defaultdict([]) # Correct d = defaultdict(list)

Another pitfall is using a mutable object as a factory, expecting a fresh copy each time. For example, defaultdict(lambda: {}) creates a new empty dict for each missing key, which is what you usually want. But defaultdict({}) would use the same empty dict for every missing key, because {} is evaluated immediately and the same object is returned each time. That is almost never the intended behavior.

Also note that defaultdict does not change the behavior of methods like get(). Calling d.get("missing") still returns None (or the optional default) without inserting a key. The default factory is only invoked by __getitem__, not by get.

Performance and Memory Considerations

The performance difference between dict and defaultdict is usually small and depends on how you use them. A defaultdict can reduce the number of explicit in checks and assignments, which may save a few operations in tight loops. However, the factory call itself adds a tiny overhead when a missing key is encountered.

In practice, the bigger cost comes from the pattern you would otherwise write with a plain dict. If you frequently need to handle missing keys, the defaultdict approach avoids repeated membership tests and conditional branches. But if missing keys are rare, a plain dict with get() might be just as fast and more explicit.

Memory usage is similar because both are hash tables. The defaultdict stores an extra reference to the factory callable, which is negligible. The real memory difference comes from what you store as default values. If the factory creates large objects, those objects are created every time a new key is inserted, so be mindful of the factory's cost.

Subclassing and Compatibility

defaultdict is a subclass of dict. It inherits all the normal dictionary methods, so you can use it anywhere a regular dict is expected. However, some code that explicitly checks type(obj) is dict will treat a defaultdict differently. If you need to pass a plain dict to a function that performs such a check, convert it explicitly with dict(obj).

When you access a missing key with __getitem__, the defaultdict inserts the key. This can be surprising if you iterate over the dictionary while also accessing keys. For example, the following loop can grow the dictionary indefinitely:

from collections import defaultdict d = defaultdict(int) d["a"] = 1 for key in d: d[key + "b"] += 1

Each iteration adds a new key, so the loop never terminates. A plain dict would raise KeyError instead, which is often safer.

Common Mistakes and Edge Cases

One edge case is using defaultdict when you want to distinguish between a missing key and a key that has a default value. Since the factory inserts a value on access, you cannot easily tell whether a key was originally absent or was created by an earlier access. If that distinction matters, use a plain dict with get() or in checks.

Another common mistake is relying on defaultdict to provide a default for pop() or setdefault(). These methods do not use the factory. setdefault will insert the default you pass, and pop will raise KeyError if the key is missing unless you provide a default argument.

from collections import defaultdict d = defaultdict(int) # This raises KeyError # d.pop("missing") # This returns the provided default value = d.pop("missing", 0)

If you need a default value for pop, pass it explicitly.

Decision Guidance

Choose a plain dict when:

  • Missing keys should be treated as errors.
  • You need to distinguish between absent and present-but-default values.
  • The set of keys is small and known in advance.
  • You want to keep the mapping behavior fully explicit.

Choose defaultdict when:

  • You are building a mapping by incrementing or appending to values.
  • The key set is dynamic and you want to avoid repeated membership checks.
  • The default factory is cheap and produces the correct initial value.
  • The fact that missing keys are inserted on access is acceptable.

In many data-processing tasks, defaultdict reduces boilerplate and makes the intent clearer. For APIs and configuration handling, a plain dict often gives you better control over validation and error reporting. The choice is not about which is more powerful; it is about which behavior matches the contract you need for missing keys.

python dict vs defaultdict: Practical Usage and Code Example | RYUSLOG DEV