LESSON 120 · DEEPER PYTHON30 min

Merge Dictionaries and Handle Duplicate Keys

Merge dictionaries while making duplicate-key behavior explicit and leaving inputs unchanged.

Example check: Worked and guided code samples are checked against CPython 3.13. Practice uses Pyodide 0.29.5 (Python 3.13) in your browser. How we check lessons

Video lesson: Merge Dictionaries and Handle Duplicate Keys

The preview is stored on this site. YouTube loads only when you press Play. Watch on YouTube

Read the video transcript

Select a timestamp to watch that moment on YouTube.

  1. 00:00

    Two configuration records both have an id field, but one says one and the other says two. If our application is combining independent records, silently taking either number would hide a source-data error. In the first run, the helper is named merge unique yet it uses a syntax that always chooses one value. Predict the result and ask whether it matches the function name. This is a good example of a bug that passes a simple demonstration with disjoint keys. We need a contract for collisions before we choose an operator. Python dictionaries can be combined in several ways, but every combination needs a rule for duplicate keys.

  2. 00:41

    The union operator makes a new dictionary and prefers the value on the right when a key appears on both sides. update changes its left-hand dictionary in place and returns None. Neither rule is automatically right for a configuration merge. When duplicates should be rejected, inspect the intersection of keys before combining records. This lesson makes the collision policy part of the function contract rather than a hidden side effect. The terminal shows the left id vanish without a warning. The dictionary still looks valid, so a later calculation may use the wrong record and be hard to trace. The problem is not that union or unpacking is defective.

  3. 01:23

    They have a documented right-wins rule. The mistake is using that rule in a function that promises to accept only unique keys. Keep the input dictionaries visible in the editor as we compare alternatives. A short expression can be concise while still hiding a policy decision. The broken helper uses an unpacking merge that silently lets the second dictionary overwrite the first. It looks short and returns the expected shape for disjoint inputs, so a happy-path example misses the problem. Add a shared key and print both the merged result and the original inputs. The collision is visible in the result even though no error was raised.

  4. 02:05

    Decide whether the inputs represent defaults and overrides, where right wins is sensible, or independent records, where a collision may indicate a data mistake. Now compare the union operator with update. Union returns a new dictionary and preserves the inputs, while update changes its receiver and returns None. Neither one rejects duplicate keys by itself. The set intersection of key views tells us exactly which keys appear on both sides. We can inspect that collision set before combining data. Notice that we compare keys, not values: two entries can have identical values and still violate a unique-key contract.

  5. 02:47

    It is the duplicated identifier that matters here. For independent records, form a set intersection of keys and raise ValueError when it is nonempty. Then use left | right to return a fresh dictionary. Choose one shared key to name in the error; do not sort arbitrary keys because different key types may not be comparable. The method should not mutate either argument. A caller can then correct a duplicate instead of losing one value. This is a policy choice; another named function could deliberately implement right-wins overrides. Avoid one vague merge helper whose behavior changes depending on context.

  6. 03:27

    The corrected helper checks for duplicate keys and raises ValueError naming one of them. Only a disjoint pair reaches the union expression. The task requires a new result and unchanged inputs, so we use union rather than update. If your product really wants defaults overridden by user settings, write a separately named function and document right-wins behavior. A single vague merge function should not silently switch policies depending on the calling page. After this run, change the sample to share host and watch the error point to that key. An informative error is useful for both automated tests and users.

  7. 04:08

    A set of shared keys has no stable display order, so choose one shared key without promising which one when several collide. Sorting arbitrary hashable keys can fail when their types are incomparable. The condition runs before any combination and before any input mutation. If no keys overlap, the union builds a new dictionary. This is still a shallow copy: mutable values inside the result may be shared with the original dictionaries. This lesson promises not to mutate dictionary containers during the merge, not to deep-copy every nested object. State that boundary if callers may later edit nested lists.

  8. 04:49

    For example, if both dictionaries contain separate lists under different keys, the result points to those same list objects. Editing a nested list later affects the input that owned it. A deep copy would require a different contract and extra cost. Keep the merge example focused on key collisions, and make the shallow nature of this operation visible to callers. The final verification uses one successful merge and one deliberate collision. It also prints the original dictionaries so we can see that neither was mutated. Test an empty input next: it should return a fresh copy of the other side. In the browser lesson, solve the practical task and then take the quiz about union,

  9. 05:32

    update, and collisions. The durable lesson is that syntax follows policy. First decide whether the right side overrides, whether duplicates are forbidden, and whether mutation is allowed. Then choose the operation whose behavior matches those promises. Test disjoint dictionaries, an empty dictionary, one shared key, mixed key types, and unchanged inputs after both successful and failed calls. Python dictionary keys are hashable, so the intersection is a set operation over keys, not a comparison of values. In modern Python, dictionary insertion order is retained, but an error should not

  10. 06:12

    assume every hashable key supports sorting. The practical task returns a new union only when no keys overlap, which keeps its promise easy to verify in the browser console.

Understand the concept

Python dictionaries can be combined in several ways, but every combination needs a rule for duplicate keys. The union operator makes a new dictionary and prefers the value on the right when a key appears on both sides. update changes its left-hand dictionary in place and returns None. Neither rule is automatically right for a configuration merge. When duplicates should be rejected, inspect the intersection of keys before combining records. This lesson makes the collision policy part of the function contract rather than a hidden side effect.

The broken helper uses an unpacking merge that silently lets the second dictionary overwrite the first. It looks short and returns the expected shape for disjoint inputs, so a happy-path example misses the problem. Add a shared key and print both the merged result and the original inputs. The collision is visible in the result even though no error was raised. Decide whether the inputs represent defaults and overrides, where right wins is sensible, or independent records, where a collision may indicate a data mistake.

For independent records, form a set intersection of keys and raise ValueError when it is nonempty. Then use left | right to return a fresh dictionary. Choose one shared key to name in the error; do not sort arbitrary keys because different key types may not be comparable. The method should not mutate either argument. A caller can then correct a duplicate instead of losing one value. This is a policy choice; another named function could deliberately implement right-wins overrides. Avoid one vague merge helper whose behavior changes depending on context.

Test disjoint dictionaries, an empty dictionary, one shared key, mixed key types, and unchanged inputs after both successful and failed calls. Python dictionary keys are hashable, so the intersection is a set operation over keys, not a comparison of values. In modern Python, dictionary insertion order is retained, but an error should not assume every hashable key supports sorting. The practical task returns a new union only when no keys overlap, which keeps its promise easy to verify in the browser console.

  • Dictionary union returns a new dictionary
  • The right operand wins duplicate keys by default
  • Key intersection detects collisions
  • A merge function should document mutation and conflict policy

See it step by step

Read the code, predict the output, then compare it with the result.

01. See the right-hand overwrite

PYTHON
left = {'mode': 'safe'}
right = {'mode': 'fast'}
print(left | right)
EXPECTED OUTPUT
{'mode': 'fast'}

Union returns a new dictionary but chooses the right value for a shared key.

02. See update mutate its receiver

PYTHON
left = {'mode': 'safe'}
result = left.update({'limit': 5})
print(left)
print(result)
EXPECTED OUTPUT
{'mode': 'safe', 'limit': 5}
None

update changes left in place and returns None.

03. Detect a key collision

PYTHON
left = {'id': 1}
right = {'id': 2, 'name': 'Ada'}
print(sorted(left.keys() & right.keys()))
EXPECTED OUTPUT
['id']

Intersecting key views identifies the shared id before it can be overwritten.

A closer look

Follow the reasoning, inspect each result, then try the suggested changes in the console below.

01 / 03

Know the default overwrite rule

Python dictionaries can be combined in several ways, but every combination needs a rule for duplicate keys. The union operator makes a new dictionary and prefers the value on the right when a key appears on both sides. update changes its left-hand dictionary in place and returns None. Neither rule is automatically right for a configuration merge. When duplicates should be rejected, inspect the intersection of keys before combining records. This lesson makes the collision policy part of the function contract rather than a hidden side effect.

Write down whether the right side is an override or an independent record. The same syntax is correct in one context and a bug in another. Run the example with no shared key, then add one and compare the results.

PYTHON
left = {'mode': 'safe'}
right = {'mode': 'fast'}
print(left | right)
EXPECTED OUTPUT
{'mode': 'fast'}
Follow the reasoning
  1. Merge two dictionaries with a shared key.
  2. Observe the right-hand value.
02 / 03

Distinguish mutation from a new result

The broken helper uses an unpacking merge that silently lets the second dictionary overwrite the first. It looks short and returns the expected shape for disjoint inputs, so a happy-path example misses the problem. Add a shared key and print both the merged result and the original inputs. The collision is visible in the result even though no error was raised. Decide whether the inputs represent defaults and overrides, where right wins is sensible, or independent records, where a collision may indicate a data mistake.

A chained expression using update often hides a None return value. Inspect the original dictionary after the call. When a helper promises not to mutate input, a copy or union is easier to reason about than an in-place update.

PYTHON
left = {'mode': 'safe'}
result = left.update({'limit': 5})
print(left)
print(result)
EXPECTED OUTPUT
{'mode': 'safe', 'limit': 5}
None
Follow the reasoning
  1. Call update and inspect both the receiver and return.
  2. State which operation mutates.
03 / 03

Reject collisions deliberately

For independent records, form a set intersection of keys and raise ValueError when it is nonempty. Then use left | right to return a fresh dictionary. Choose one shared key to name in the error; do not sort arbitrary keys because different key types may not be comparable. The method should not mutate either argument. A caller can then correct a duplicate instead of losing one value. This is a policy choice; another named function could deliberately implement right-wins overrides. Avoid one vague merge helper whose behavior changes depending on context.

Choose one shared key to report without sorting arbitrary hashable keys. The task tests the failure path as well as the successful one; the failure should happen before any input mutation.

PYTHON
left = {'id': 1}
right = {'id': 2, 'name': 'Ada'}
print(sorted(left.keys() & right.keys()))
EXPECTED OUTPUT
['id']
Follow the reasoning
  1. Intersect key views before union.
  2. Reject the duplicate rather than overwriting it.

Try it in Python

Edit the example and run it. Python starts in your browser the first time you click Run.

Python console

Ready to run
Need input()? Add one value per line
Ctrl / ⌘ + Enter to run
OUTPUT
Your output appears here.
Your turnComplete merge_unique(left, right). Return a new dictionary when keys are disjoint. If any key appears in both inputs, raise ValueError with a message containing the duplicate key. Never mutate either input. The prepared example should print {'host': 'db', 'port': 5432}.
Need a hint?

Compute duplicates = left.keys() & right.keys(). If nonempty, raise ValueError naming next(iter(duplicates)); otherwise return left | right.

Complete the task and select Check task to verify your code.

    Quick quiz

    Three questions. You can change your answers and try again.

    01.What happens to a shared key in left | right?

    02.What does dict.update return?

    03.Why test shared keys before a unique merge?

    Typical mistakes

    Everyone meets these errors. See what causes them and how to fix them.

    Assuming union rejects collisions

    COMMON MISTAKE
    combined = left | right
    FIX
    if left.keys() & right.keys():
        raise ValueError('duplicate key')
    combined = left | right

    What happens: A duplicate right-hand value silently replaces the left value.
    Check the policy before applying union.

    Assigning update's return value

    COMMON MISTAKE
    combined = left.update(right)
    FIX
    combined = left | right

    What happens: combined is None and left has changed.
    Use union for a fresh result, or call update when in-place mutation is intended.

    Browse this lesson’s problem fixes

    Sources and further reading