How to use ** in Python
Learn how to use Python's ** operator for exponentiation and dictionary unpacking. Explore tips, real-world examples, and common errors.
.png)
The ** operator in Python is a powerful tool for exponentiation and dictionary unpacking. It offers a concise way to handle keyword arguments and merge dictionaries, simplifying complex operations.
You'll explore techniques for both uses of the operator. You'll also find practical tips, real-world applications, and debugging advice to help you use ** with confidence.
Basic exponentiation with **
x = 2 ** 3 # 2 raised to the power of 3
y = 5 ** 2 # 5 squared
print(f"2 to the power of 3 is {x}")
print(f"5 squared is {y}")--OUTPUT--2 to the power of 3 is 8
5 squared is 25
The ** operator provides a clean, readable syntax for exponentiation. In the example, 2 ** 3 directly calculates 2 raised to the power of 3, and 5 ** 2 computes 5 squared. This makes the mathematical intent of your code immediately clear at a glance.
While Python's built-in pow() function can achieve the same result, using the ** operator is often more Pythonic and concise for simple arithmetic. It allows you to embed the calculation directly into expressions, improving code flow and readability without the overhead of a function call.
Dictionary unpacking with **
Just as the ** operator simplifies exponentiation, it also streamlines dictionary operations, letting you unpack key-value pairs into function calls, accept flexible arguments, and merge dictionaries.
Using ** to unpack dictionaries in function calls
def greet(name, age, city):
return f"Hello {name}, you are {age} years old and from {city}."
user_info = {"name": "Alice", "age": 30, "city": "New York"}
print(greet(**user_info))--OUTPUT--Hello Alice, you are 30 years old and from New York.
When you use **user_info, you're telling Python to unpack the dictionary and use its contents as keyword arguments for the greet() function. It’s a clean shortcut for calling greet(name="Alice", age=30, city="New York") without listing each argument manually.
- For this to work, the dictionary keys must exactly match the function's parameter names.
- This technique is especially useful when function arguments come from dynamic sources like API responses or database records, which are often structured as dictionaries.
Accepting keyword arguments with **kwargs
def process_user(**kwargs):
for key, value in kwargs.items():
print(f"The user's {key} is {value}")
process_user(name="Bob", age=25, job="Developer")--OUTPUT--The user's name is Bob
The user's age is 25
The user's job is Developer
The **kwargs parameter in the process_user() function definition acts as a catch-all for keyword arguments. Python bundles any keyword arguments that aren't explicitly defined as parameters into a dictionary named kwargs. This makes your functions incredibly flexible, as they can handle a variable number of inputs without modification.
- Inside the function, you can iterate over
kwargsjust like a standard dictionary to access each key-value pair. - This pattern is especially useful for functions that process optional data or pass arguments through to other functions.
Merging dictionaries with **
defaults = {"color": "blue", "size": "medium"}
preferences = {"color": "red", "material": "cotton"}
settings = {**defaults, **preferences}
print(settings)--OUTPUT--{'color': 'red', 'size': 'medium', 'material': 'cotton'}
The ** operator provides an elegant syntax for merging dictionaries. When you unpack **defaults and **preferences inside new curly braces, you're creating a single dictionary that combines the contents of both.
- The order is important. If both dictionaries share a key, the value from the rightmost dictionary—in this case,
preferences—wins. That's whycoloris"red". - Unique keys from either dictionary, like
material, are simply added to the new dictionary.
Advanced ** techniques
With the core concepts covered, you can now leverage the ** operator for more nuanced tasks, from streamlining class creation to navigating nested dictionaries.
Using ** with class constructors and inheritance
class Parent:
def __init__(self, a, b):
self.a = a
self.b = b
class Child(Parent):
def __init__(self, c, **kwargs):
super().__init__(**kwargs)
self.c = c
obj = Child(c=3, a=1, b=2)
print(vars(obj))--OUTPUT--{'a': 1, 'b': 2, 'c': 3}
The ** operator is a game-changer for class inheritance. In the Child class, **kwargs collects any keyword arguments that aren't explicitly defined in its constructor, such as a and b.
These arguments are then neatly passed to the Parent constructor using super().__init__(**kwargs). This technique allows you to:
- Forward arguments to a parent class without needing to name them individually.
- Keep the child's initializer clean, focusing only on its own specific parameters like
c.
It's a powerful way to manage complex initializations in object-oriented programming.
How order affects ** unpacking results
config1 = {"debug": False, "timeout": 30}
config2 = {"debug": True, "verbose": True}
merged1 = {**config1, **config2}
merged2 = {**config2, **config1}
print(f"config2 overrides config1: {merged1}")
print(f"config1 overrides config2: {merged2}")--OUTPUT--config2 overrides config1: {'debug': True, 'timeout': 30, 'verbose': True}
config1 overrides config2: {'debug': False, 'verbose': True, 'timeout': 30}
When you merge dictionaries with the ** operator, the order of unpacking is crucial. If two dictionaries share a key, the value from the dictionary unpacked last will overwrite the earlier one. In the example, merged1 unpacks config2 after config1, so the final debug value becomes True.
- Conversely, in
merged2,config1is unpacked last, so itsdebugvalue ofFalseis used instead. - Keys that are unique to either dictionary, like
timeoutandverbose, are always included in the final result.
Nested dictionary unpacking with **
user = {"name": "Charlie", "profile": {"age": 28, "role": "Admin"}}
defaults = {"active": True, "profile": {"level": 1, "verified": False}}
result = {**defaults, **user, "profile": {**defaults["profile"], **user["profile"]}}
print(result)--OUTPUT--{'active': True, 'profile': {'level': 1, 'verified': False, 'age': 28, 'role': 'Admin'}, 'name': 'Charlie'}
The ** operator performs a shallow merge, meaning it only unpacks the top-level keys. If you simply used {**defaults, **user}, the entire profile dictionary from user would overwrite the one from defaults, losing nested values like "level" and "verified".
To properly combine nested dictionaries, you must handle them explicitly.
- The code first unpacks the top-level dictionaries, then immediately redefines the
profilekey. - Inside, it unpacks the nested dictionaries
defaults["profile"]anduser["profile"], effectively merging their contents into a single, combined dictionary.
Move faster with Replit
Replit is an AI-powered development platform that transforms natural language into working applications. Describe what you want to build, and Replit Agent creates it—complete with databases, APIs, and deployment.
For the ** operator techniques you've just explored, Replit Agent can turn them into production-ready tools:
- Build a financial calculator that models compound interest using the
**operator for exponentiation. - Create a configuration utility that merges default settings with user preferences by unpacking dictionaries with
**. - Deploy a flexible API that accepts and processes a variable number of optional parameters using
**kwargs.
Describe your app idea, and Replit Agent will write the code, test it, and fix issues automatically, all inside your browser.
Common errors and challenges
While the ** operator is powerful, you might run into a few common pitfalls, from type errors to unexpected merge results.
- A frequent mistake is attempting to unpack a non-dictionary object. The
**operator is designed specifically for mappings, so using it on a list or set will raise aTypeErrorbecause Python expects key-value pairs, not just a sequence of items. - You'll also encounter a
TypeErrorif you unpack a dictionary into a function call but the dictionary is missing a required argument. If a function expects acityargument and your dictionary doesn't have that key, the call will fail. It's good practice to ensure your dictionary contains all necessary keys or to define your function with default values for optional parameters. - Remember that
**performs a shallow merge, which can be tricky when dealing with nested dictionaries. As shown earlier, simply unpacking two dictionaries will cause the nested dictionary from the second to completely overwrite the first. You must merge the nested dictionaries explicitly to combine their contents properly.
Fixing TypeError when unpacking non-dictionary objects
You'll hit a TypeError if you try to unpack a non-dictionary object with the ** operator. Python expects a mapping of key-value pairs, not a sequence like a list. The code below demonstrates this common mistake in action.
user_data = [("name", "David"), ("age", 28)]
def display_user(name, age):
print(f"{name} is {age} years old")
display_user(**user_data) # TypeError: 'list' object is not a mapping
The ** operator fails here because user_data is a list, not a dictionary. It can't map the list's tuples to the function's keyword arguments. The fix is to convert the data into the expected dictionary structure.
user_data = [("name", "David"), ("age", 28)]
def display_user(name, age):
print(f"{name} is {age} years old")
user_dict = dict(user_data)
display_user(**user_dict)
The solution is to convert the list of tuples into a dictionary with dict() before unpacking. The ** operator can't work with a list because it needs a key-value mapping to match function arguments. This error is common when your data, like from a database query, comes as a list of pairs instead of a dictionary. The conversion provides the correct structure, allowing ** to unpack the data into keyword arguments as intended.
Dealing with missing required arguments when unpacking with **
You'll hit a TypeError when unpacking a dictionary that's missing a required function argument. The ** operator can't fill in the blanks, so the call fails if a key is absent. The code below shows this exact problem in action.
def create_user(username, email, role):
print(f"Created {role} user {username} with email {email}")
user_data = {"username": "john_doe", "email": "john@example.com"}
create_user(**user_data) # TypeError: missing 1 required positional argument: 'role'
The create_user() function's signature requires a role, but the user_data dictionary doesn't provide one. When unpacked, the function call is incomplete, causing the TypeError. The code below shows how to handle this scenario.
def create_user(username, email, role):
print(f"Created {role} user {username} with email {email}")
user_data = {"username": "john_doe", "email": "john@example.com"}
user_data["role"] = "user"
create_user(**user_data)
The solution is to ensure the dictionary contains every required argument before you unpack it. The create_user() function expects a role, but the user_data dictionary is missing it. By adding the key with user_data["role"] = "user", you satisfy the function's signature and resolve the error.
- This issue often arises when your data comes from sources like APIs or databases, where certain fields might not always be present.
Properly merging nested dictionaries with **
A common pitfall with the ** operator is its shallow merge behavior. When you combine dictionaries, a nested dictionary from the second one will completely overwrite the first, causing you to lose data. The following code shows this in action.
defaults = {"settings": {"timeout": 30, "retries": 3}, "debug": False}
user_config = {"settings": {"timeout": 60}, "verbose": True}
merged = {**defaults, **user_config}
print(merged) # "settings" is completely overwritten
The ** operator replaces the entire settings dictionary from defaults with the one from user_config, causing the retries key to be lost. The code below demonstrates how to merge the nested dictionaries correctly.
defaults = {"settings": {"timeout": 30, "retries": 3}, "debug": False}
user_config = {"settings": {"timeout": 60}, "verbose": True}
merged = {
**defaults,
**user_config,
"settings": {**defaults["settings"], **user_config["settings"]}
}
print(merged)
The solution is to merge the nested dictionaries explicitly. The code first unpacks the top-level dictionaries, then immediately redefines the settings key. Inside, it unpacks the nested dictionaries from both defaults and user_config. This combines their contents, preserving all keys while letting user-specific values override the defaults. You'll find this technique essential when managing layered configurations, like default settings that need to be updated with user preferences.
Real-world applications
With a solid grasp of its mechanics and pitfalls, you can use the ** operator to build robust configuration systems and dynamic API requests.
Creating configuration systems with **
The ** operator is ideal for building layered configuration systems, allowing you to merge default, system, and user settings with a clear order of precedence.
def create_app_config(**user_overrides):
# Default configuration
defaults = {
"debug": False,
"theme": "light",
"max_upload_size": 5,
"timeout": 30
}
# System-wide configuration
system_config = {"debug": True, "theme": "dark"}
# Final config combines all settings with user overrides taking priority
final_config = {**defaults, **system_config, **user_overrides}
return final_config
config = create_app_config(max_upload_size=10, cache_enabled=True)
print(config)
The create_app_config function uses **user_overrides to capture any keyword arguments you pass. It then builds a final configuration by unpacking three dictionaries in a specific sequence, with later values overwriting earlier ones.
- First, it establishes the
defaults. - Next, it applies the
system_config, which overrides shared keys likedebugandtheme. - Finally, your
user_overridesare unpacked, giving them the highest priority to add new settings or change existing ones.
This method ensures that user-provided settings always have the final say.
Building API requests with **
The ** operator is also perfect for constructing dynamic API requests, allowing you to combine fixed parameters like authentication keys with flexible query arguments.
def make_api_request(endpoint, **query_params):
# Base parameters used in all requests
auth_params = {"api_key": "xyz123", "format": "json"}
# Combine auth parameters with query parameters
params = {**auth_params, **query_params}
# Simulate API request (would use requests library in real code)
url = f"https://api.example.com{endpoint}"
print(f"Requesting: {url}")
print(f"With parameters: {params}")
make_api_request("/products/search", query="python books", sort="relevance", limit=5)
The make_api_request function uses **query_params to dynamically accept any number of keyword arguments, making it highly adaptable. This is useful when you don't know in advance which parameters will be needed for a request.
- Inside the function, the
**operator unpacks both the predefinedauth_paramsand your incomingquery_paramsinto a new dictionary. - Because
query_paramsis unpacked last, any arguments you provide will overwrite the default authentication parameters if their keys match.
Get started with Replit
Turn your knowledge of the ** operator into a real tool. Describe what you want to build to Replit Agent, like "a compound interest calculator" or "a utility that merges default and user configuration files".
Replit Agent writes the code, tests for errors, and deploys your application. Start building with Replit.
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 & 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.

.png)
.png)
.png)