python
dict.update
object-oriented programming
mutable types
python methods

Why doesn't a python dict.update return the object?

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

dict.update() mutates a dictionary in place and returns None, which feels surprising if you expect fluent method chaining. In Python, though, that behavior is deliberate and consistent with the design of many mutable container methods. The language uses the return value to signal whether a method is meant for side effects or meant to produce a new object.

Python Uses a Convention for Mutable Containers

Several built-in mutable types follow the same pattern as dict.update().

  • 'list.sort() sorts in place and returns None'
  • 'list.reverse() reverses in place and returns None'
  • 'set.update() mutates in place and returns None'

That convention makes side effects visible. When you see one of these methods, the message is “this object changes here.”

python
1items = [3, 1, 2]
2result = items.sort()
3
4print(result)
5print(items)

The output shows the intent clearly: the list changed, and there is no separate result object to capture.

What dict.update() Actually Does

update() takes key-value pairs from another mapping or iterable and writes them into the existing dictionary.

python
1base = {"host": "localhost", "port": 5432}
2override = {"port": 5433, "ssl": True}
3
4base.update(override)
5print(base)

Two important things happen:

  • existing keys are overwritten by incoming values
  • new keys are added to the same dictionary object

That means the identity of base does not change. Only its contents change.

Why Returning the Dictionary Would Be Misleading

If update() returned the same dictionary, many people would start chaining mutation-heavy expressions and treating side-effecting code as though it were purely functional. Python generally discourages that style for built-in mutable containers.

The design goal is clarity. A statement such as config.update(env_overrides) reads like a mutation. An expression such as config = config.update(env_overrides) would suggest a new value is being built, even though the original object is being modified.

That distinction matters in debugging, code review, and reasoning about aliasing. If two variables reference the same dictionary, in-place mutation affects both views of that object.

The Common Mistake

The classic bug is capturing the return value.

python
1a = {"x": 1}
2b = {"y": 2}
3
4merged = a.update(b)
5print(merged)
6print(a)

merged is None, because the real result is that a was changed.

The correct version depends on your goal.

If you want mutation:

python
a.update(b)
print(a)

If you want a new dictionary instead:

python
1a = {"x": 1}
2b = {"y": 2}
3merged = {**a, **b}
4print(merged)

And in modern Python:

python
1a = {"x": 1}
2b = {"y": 2}
3merged = a | b
4print(merged)

That last form is often the cleanest non-mutating merge.

API Design Lesson: Mutation and Pure Results Should Be Separate

This convention is useful when designing your own classes as well. A method that mutates state should generally make that obvious. A method that returns a transformed value should usually not mutate the original object.

python
1class CounterMap(dict):
2    def bump(self, key, amount=1):
3        self[key] = self.get(key, 0) + amount
4
5    def bumped(self, key, amount=1):
6        clone = CounterMap(self)
7        clone.bump(key, amount)
8        return clone
9
10counts = CounterMap({"errors": 2})
11counts.bump("errors")
12next_counts = counts.bumped("warnings")
13
14print(counts)
15print(next_counts)

The names tell the story: one method mutates, the other returns a modified copy.

Choose the Right Merge Style for the Use Case

There is no single best merge style. In-place updates are good when you deliberately maintain mutable state, such as loading configuration in stages. New-object merges are better when you want predictable immutable-style flow.

python
1def build_config(defaults, env, cli):
2    return defaults | env | cli
3
4print(build_config({"debug": False}, {"debug": True}, {"port": 8080}))

That style keeps the original dictionaries unchanged and makes precedence order explicit.

Common Pitfalls

  • Assigning the return value of dict.update() and expecting a merged dictionary.
  • Forgetting that update() mutates the original object and therefore affects every reference to it.
  • Mixing in-place and immutable merge styles inconsistently within the same code path.
  • Using update() in a fluent chain even though the built-in API is designed to signal side effects with None.
  • Failing to document precedence when multiple dictionaries are merged in sequence.

Summary

  • 'dict.update() returns None because it mutates a dictionary in place.'
  • That behavior matches a broader Python convention for mutable container methods.
  • The design helps distinguish side-effecting operations from value-producing ones.
  • Use update() when mutation is intended, and use merge expressions when you need a new dictionary.
  • Clear separation between mutation and transformation makes Python code easier to reason about.

Course illustration
Course illustration

All Rights Reserved.