How to unpack a dictionary in Python
Learn how to unpack a Python dictionary with various methods. Explore tips, real-world applications, and common error debugging techniques.
.avif)
In Python, you can unpack a dictionary to pass its key-value pairs as keyword arguments to a function. This technique uses the ** operator to simplify code and improve readability.
In this article, we'll explore several methods, practical tips, and real-world applications. You'll also get advice to debug common issues and master this skill for cleaner, more efficient code.
Using the ** operator for basic dictionary unpacking
original_dict = {'a': 1, 'b': 2, 'c': 3}
new_dict = {**original_dict}
print(new_dict)--OUTPUT--{'a': 1, 'b': 2, 'c': 3}
The ** operator unpacks the key-value pairs from original_dict directly within the new dictionary’s definition. This syntax effectively expands its contents in place, creating a shallow copy.
This is a concise and modern way to duplicate a dictionary, ensuring that subsequent changes to new_dict won't accidentally alter original_dict. It’s a popular alternative to using the dict.copy() method.
Common dictionary unpacking techniques
Beyond simple duplication, the ** operator offers powerful ways to merge dictionaries, pass keyword arguments to functions, and work with comprehensions.
Unpacking dictionaries into function arguments with **
def greet(name, age, city):
return f"{name}, age {age}, lives in {city}"
person = {'name': 'Alice', 'age': 30, 'city': 'New York'}
print(greet(**person))--OUTPUT--Alice, age 30, lives in New York
The ** operator unpacks the person dictionary, automatically matching its keys to the greet function's parameters. This lets you pass name, age, and city as keyword arguments without writing them out individually. It’s a clean way to call functions when your data is already structured in a dictionary.
- For this to work, the dictionary keys must exactly match the function's parameter names.
- This technique simplifies function calls, making your code more readable and maintainable, especially with functions that have many arguments.
Merging dictionaries with unpacking
dict1 = {'a': 1, 'b': 2}
dict2 = {'b': 3, 'c': 4}
merged = {**dict1, **dict2}
print(merged)--OUTPUT--{'a': 1, 'b': 3, 'c': 4}
You can combine multiple dictionaries by unpacking them into a new one. The expression {**dict1, **dict2} creates a single merged dictionary containing elements from both.
- Notice what happens with overlapping keys. The key
'b'exists in both dictionaries, but the final output uses the value fromdict2. - This is because unpacking happens from left to right, so the value from the last dictionary unpacked always wins.
Unpacking with dictionary comprehensions
original = {'a': 1, 'b': 2, 'c': 3, 'd': 4}
filtered = {**{k: v for k, v in original.items() if v % 2 == 0}}
print(filtered)--OUTPUT--{'b': 2, 'd': 4}
You can also use the ** operator with a dictionary comprehension to build a new dictionary from filtered or transformed items. The comprehension first creates a temporary dictionary—in this case, by selecting only the items from original with even values.
- The
**operator then unpacks this temporary dictionary's contents into the finalfiltereddictionary. - While this demonstrates how the features combine, you can often achieve the same result more directly with just the comprehension itself.
Advanced dictionary unpacking techniques
Beyond these common techniques, you can also use the ** operator to handle more nuanced scenarios, like applying conditional logic or custom transformations during unpacking.
Conditional unpacking with the ** operator
include_extra = True
base_config = {'debug': False, 'log_level': 'INFO'}
full_config = {**base_config, **({"extra_logging": True} if include_extra else {})}
print(full_config)--OUTPUT--{'debug': False, 'log_level': 'INFO', 'extra_logging': True}
This technique lets you conditionally add key-value pairs to a dictionary. The expression **({"extra_logging": True} if include_extra else {}) is the key. It uses a conditional to decide which dictionary to unpack, making it perfect for managing optional settings.
- If
include_extraisTrue, the expression evaluates to a dictionary containing the extra key, which is then unpacked intofull_config. - If it’s
False, the expression becomes an empty dictionary{}. Unpacking an empty dictionary adds nothing, effectively skipping the optional keys.
Handling duplicate keys with custom logic
def merge_with_max(dict1, dict2):
common_keys = set(dict1.keys()) & set(dict2.keys())
return {**dict1, **dict2, **{k: max(dict1[k], dict2[k]) for k in common_keys}}
print(merge_with_max({'a': 1, 'b': 5}, {'a': 3, 'c': 6}))--OUTPUT--{'a': 3, 'b': 5, 'c': 6}
When you need more control over merging than simply overwriting keys, you can combine multiple unpacking steps. The merge_with_max function shows how to apply custom logic to resolve duplicate keys instead of just taking the last-seen value.
- First, it finds all keys shared between both dictionaries using a set intersection (
&). - It then uses a dictionary comprehension to build a third, temporary dictionary that contains only the common keys, with their values set to the maximum of the two originals.
- Finally, all three are unpacked. Because unpacking happens from left to right, this third dictionary is unpacked last, ensuring its values overwrite any previous ones.
Unpacking with dictionary transformations
user_data = {'username': 'jsmith', 'first_name': 'John', 'last_name': 'Smith'}
defaults = {'active': True, 'admin': False}
transformed = {**defaults, **user_data, 'full_name': f"{user_data['first_name']} {user_data['last_name']}"}
print(transformed)--OUTPUT--{'active': True, 'admin': False, 'username': 'jsmith', 'first_name': 'John', 'last_name': 'Smith', 'full_name': 'John Smith'}
You can combine dictionary unpacking with other operations to create transformed dictionaries in a single expression. This example merges defaults and user_data while also adding a new, computed field. It's a clean way to enrich data structures on the fly.
- First,
**defaultsand**user_dataare unpacked into the new dictionary. - Then, a new key-value pair,
'full_name', is added. Its value is created dynamically using an f-string that pulls from the originaluser_data.
This approach lets you set defaults, merge data, and add transformed fields all in one readable line.
Move faster with Replit
Replit is an AI-powered development platform where you can start coding Python instantly. All the necessary dependencies come pre-installed, so you can skip the setup and get straight to building.
Instead of just piecing together techniques like dictionary unpacking, you can use Agent 4 to build complete applications. It takes your idea and builds a working product—handling the code, databases, APIs, and deployment from a description.
For example, you could describe tools that use dictionary unpacking to:
- Build a configuration manager that merges a base settings dictionary with user-specific overrides.
- Create a dynamic report generator that unpacks a dictionary of data into a function’s keyword arguments.
- Develop a data enrichment utility that combines multiple data sources and adds new, computed fields on the fly.
Simply describe your app, and Replit will write the code, test it, and fix issues automatically, all within your browser.
Common errors and challenges
While the ** operator is powerful, you might encounter common issues like a TypeError or unexpected results with nested dictionaries.
Troubleshooting TypeError when unpacking non-dictionary objects
The ** operator is designed exclusively for dictionaries. Attempting to use it on another iterable, like a list, will trigger a TypeError because Python can't map keys to values. The code below shows what happens when you try it.
config = {'debug': True}
values = [1, 2, 3]
result = {**config, **values}
print(result)
The error occurs because the ** operator requires key-value pairs, but the list values lacks keys. Python can't unpack it into a dictionary. The following example shows the correct approach to resolve this.
config = {'debug': True}
values = [1, 2, 3]
values_dict = {str(i): val for i, val in enumerate(values)}
result = {**config, **values_dict}
print(result)
To fix the TypeError, you first need to convert the list into a dictionary. The solution does this with a dictionary comprehension using enumerate(). This function generates key-value pairs by pairing each item from the list with its index, which is then converted to a string key. Once the list is a proper dictionary, values_dict, you can unpack it with the ** operator without errors. This issue often arises when merging data from mixed iterable types.
Fixing unexpected results with nested dictionaries and **
Fixing unexpected results with nested dictionaries and **
The ** operator performs a shallow merge, which can cause unexpected behavior with nested dictionaries. Instead of combining the inner dictionaries, the operator overwrites them completely, leading to lost data. The code below shows this in action.
defaults = {'settings': {'timeout': 30, 'retries': 3}}
user_config = {'settings': {'timeout': 60}}
merged = {**defaults, **user_config}
print(merged['settings'])
The user_config dictionary's settings key overwrites the one from defaults, so the retries key is lost. The ** operator doesn't recursively merge nested dictionaries. Check the code below for the correct implementation.
defaults = {'settings': {'timeout': 30, 'retries': 3}}
user_config = {'settings': {'timeout': 60}}
merged = {**defaults, 'settings': {**defaults['settings'], **user_config['settings']}}
print(merged['settings'])
To fix this, you need to merge the nested dictionaries manually. The solution unpacks defaults['settings'] and then user_config['settings'] inside a new dictionary assigned to the settings key. This creates a combined inner dictionary, preserving keys like retries while updating timeout.
This is a crucial pattern when you're working with complex configurations, like application settings, where you only want to override specific nested values without losing others.
Avoiding KeyError when working with unpacked dictionaries
A KeyError happens when you try to access a dictionary key that doesn't exist. After merging dictionaries with the ** operator, it's easy to mistakenly assume a key is present, which can crash your program. The following code shows this error.
user_data = {'name': 'Alice'}
settings = {'theme': 'dark'}
combined = {**user_data, **settings}
email = combined['email']
The code tries to access the email key, but it doesn't exist in the combined dictionary. Since neither source dictionary contained it, the lookup fails. The following example shows how to prevent this error.
user_data = {'name': 'Alice'}
settings = {'theme': 'dark'}
combined = {**user_data, **settings}
email = combined.get('email', 'default@example.com')
To prevent a KeyError, use the .get() method instead of direct key access with []. The .get() method safely looks for a key and returns a default value if it’s not found, which keeps your program from crashing.
This approach is essential when merging dictionaries from different sources or handling optional data, as you can’t always guarantee a key will be present in the final dictionary.
Real-world applications
Moving past the common errors, the ** operator becomes essential for real-world tasks like managing configurations and handling API data.
Using the ** operator for configuration management with fallbacks
The ** operator is ideal for managing application settings by layering multiple configuration sources, allowing specific options to override general defaults.
default_config = {'timeout': 30, 'retries': 3, 'debug': False}
user_config = {'timeout': 60, 'log_level': 'INFO'}
env_config = {'debug': True}
final_config = {**default_config, **user_config, **env_config}
print(final_config)
This example shows how the ** operator combines multiple dictionaries. The order in which you unpack them is critical because it dictates which values are kept when keys are duplicated across the dictionaries.
- Since
user_configis unpacked afterdefault_config, itstimeoutvalue of60is used. - Likewise,
env_configis unpacked last, so itsdebugvalue ofTruewins out.
For any shared key, the value from the rightmost dictionary in the expression is the one that makes it into the final result.
Processing API request parameters with the ** operator
The ** operator is also effective for processing API request parameters, allowing you to set defaults and normalize inconsistent keys on the fly.
def process_api_request(endpoint, **params):
processed_params = {
**{'format': 'json', 'version': 'v1'},
**{k.lower(): v for k, v in params.items()},
'timestamp': '2023-11-08'
}
return f"Requesting {endpoint} with parameters: {processed_params}"
result = process_api_request('users', ID=123, Filter='active', FORMAT='xml')
print(result)
The process_api_request function uses **params to accept any keyword arguments, giving you flexibility. It builds a final parameter dictionary by combining multiple sources in a specific order.
- First, it unpacks a dictionary of base settings like
'format': 'json'. - Next, it unpacks the incoming
paramsafter a dictionary comprehension converts all keys to lowercase. This allows a parameter likeFORMAT='xml'to override the default. - Finally, it adds a static
timestampkey.
This layered approach creates a clean set of parameters for the API call.
Get started with Replit
Turn these unpacking techniques into a real tool. Describe what you want to build to Replit Agent, like “a config loader that merges default and user settings” or “a utility to process API parameters with fallbacks.”
Replit Agent writes the code, tests for errors, and deploys your application from your description. Start building with Replit today.
Create and deploy websites, automations, internal tools, data pipelines and more in any programming language without setup, downloads or extra tools. All in a single cloud workspace with AI built in.
Create and deploy websites, automations, internal tools, data pipelines and more in any programming language without setup, downloads or extra tools. All in a single cloud workspace with AI built in.


.avif)
.avif)