Back to Blog
Python

Python Dictionary Unpacking: Syntax and Use Cases

python dictionary unpacking: Learn how to use the ** operator to unpack dictionaries into function calls, merge dictionaries, and handle edge cases in Python.

dictionaryunpackingkwargsfunction argumentsmerging dictionaries
Illustration of Python dictionary unpacking with the double asterisk operator merging two dictionaries into one.

python dictionary unpacking requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.

Python's dictionary unpacking via the ** operator is a concise way to spread dictionary items into function calls, merge dictionaries, and build new dictionaries from existing ones. It appears throughout modern Python code, but its behavior has subtle rules that can surprise developers who are new to it.

The ** Operator in Function Calls

The most common use of dictionary unpacking is passing a dictionary as keyword arguments to a function. When you place ** before a dictionary in a function call, Python expands the dictionary into keyword arguments using the keys as parameter names and the values as argument values.

def configure(host, port, debug=False): print(f"{host}:{port}, debug={debug}") settings = {"host": "localhost", "port": 8080, "debug": True} configure(**settings)

This works because the keys match the parameter names. The dictionary keys must be strings, and they must correspond to valid parameter names. If a key does not match any parameter, Python raises a TypeError with an unexpected keyword argument message. If a key matches a parameter that is already provided explicitly, the explicit argument wins, but the unpacked value is still evaluated.

configure("example.com", **{"port": 443, "debug": False})

Here host is passed positionally, while port and debug come from the dictionary. The order of evaluation is left to right, so an explicit positional argument is assigned before the unpacked values are applied.

Merging Dictionaries with Unpacking

Dictionary unpacking also provides a clean way to merge dictionaries into a new one. The syntax {**dict1, **dict2} creates a new dictionary that contains all items from both inputs. When keys overlap, the later dictionary overrides the earlier one.

base = {"name": "app", "version": 1} update = {"version": 2, "author": "team"} merged = {**base, **update} print(merged) # {'name': 'app', 'version': 2, 'author': 'team'}

This pattern is often used to apply configuration overrides without mutating the original dictionary. It is also the recommended way to merge dictionaries in Python 3.5 and later, before the | operator was introduced in Python 3.9. The | operator provides a more readable alternative, but ** remains useful when you need to merge multiple dictionaries in a single expression or when you are working with code that must support older Python versions.

Unpacking in Function Definitions

Dictionary unpacking is not limited to function calls. In function definitions, **kwargs collects any number of keyword arguments into a dictionary. This is the inverse operation: instead of spreading a dictionary into arguments, it gathers arguments into a dictionary.

def log_event(event_type, **details): print(f"{event_type}: {details}") log_event("login", user="alice", ip="10.0.0.1")

Inside the function, details is a regular dictionary with string keys. This pattern is useful for forwarding arguments to other functions or for building flexible APIs that accept optional metadata. When combined with unpacking in calls, it enables a clean delegation pattern:

def wrapper(*args, **kwargs): return target(*args, **kwargs)

This preserves both positional and keyword arguments while allowing the wrapper to add behavior before or after the call.

Duplicate Keys and Override Order

When merging dictionaries with **, the order of the dictionaries determines which value survives for duplicate keys. In {**a, **b}, any key that appears in both a and b takes its value from b. This is consistent with the left-to-right evaluation of the dictionary display.

a = {"x": 1, "y": 2} b = {"y": 3, "z": 4} result = {**a, **b} # {'x': 1, 'y': 3, 'z': 4}

If you need the opposite precedence, swap the order. This behavior is intuitive but can cause subtle bugs when dictionaries are merged in a loop or from a dynamic list. For example, merging a list of dictionaries in order gives the last dictionary the highest priority, which may not be what you intend if the list is not sorted by importance.

Non-String Keys and Other Edge Cases

The ** operator requires dictionary keys to be strings when used in function calls or function definitions. In a dictionary display like {**d}, the keys can be any hashable type, but the result is still a dictionary with those keys. This distinction matters because unpacking into a function call enforces the string-key rule, while merging dictionaries does not.

d = {1: "one", 2: "two"} # This works for merging: merged = {**d, 3: "three"} # {1: 'one', 2: 'two', 3: 'three'} # This fails in a function call: def f(a, b): pass f(**d) # TypeError: f() got an unexpected keyword argument '1'

Another edge case involves unpacking an empty dictionary. {**{}} produces an empty dictionary, and f(**{}) calls f with no keyword arguments. This is harmless but can mask a missing configuration if the empty dictionary comes from a variable that was expected to contain data.

Performance and Memory Considerations

Merging dictionaries with ** creates a new dictionary and copies all items. This is O(n) in the total number of keys, and it allocates a new object. For small dictionaries the overhead is negligible, but in a loop that merges many dictionaries repeatedly, the allocation cost can add up.

# Repeated merging in a loop creates a new dict each iteration combined = {} for d in list_of_dicts: combined = {**combined, **d}

This pattern is O(n²) because each iteration copies all previously accumulated items. If you need to merge many dictionaries and the number of keys is large, consider using dict.update in a loop to mutate a single dictionary, or use the | operator if you are on Python 3.9+ and can rely on the in-place update semantics of |=.

combined = {} for d in list_of_dicts: combined.update(d)

This avoids the repeated allocation and is more memory-efficient. However, it mutates combined, so it is not suitable when you need to preserve the original dictionaries unchanged. The tradeoff between immutability and performance should guide your choice.

Readability and Maintainability Tradeoffs

Dictionary unpacking makes code concise, but it can reduce readability when overused. A chain of ** merges in a single expression is harder to follow than an explicit loop or a series of update calls. The intent is clear when you are merging two or three dictionaries, but beyond that, consider using a helper function or a more explicit approach.

# Hard to read at a glance config = {**defaults, **user_settings, **env_overrides, **cli_args} # More explicit but verbose config = defaults.copy() config.update(user_settings) config.update(env_overrides) config.update(cli_args)

The explicit version makes the precedence order visible and is easier to debug. It also allows you to add logging or validation between updates. The ** syntax is best reserved for cases where the merge is a single logical step and the precedence order is obvious from the variable names.

Another maintainability concern is that unpacking hides the parameter names in a function call. When you see f(**data), you cannot tell which keys are expected without inspecting the function definition. This indirection can be useful for generic forwarding, but it makes the code harder to statically analyze and can obscure errors. Tools like type checkers and IDEs may not be able to validate the keyword arguments, so use this pattern sparingly in public APIs where clarity matters.

In summary, dictionary unpacking is a powerful feature that serves three main purposes: passing keyword arguments, merging dictionaries, and collecting keyword arguments in function definitions. Understanding the override order, the string-key requirement for function calls, and the performance characteristics of repeated merges will help you use it effectively without introducing subtle bugs.

python dictionary unpacking: Practical Usage and Code Exampl | RYUSLOG DEV