Table of contents
Why Gabriel Joined a Bootcamp

Python ** Operator: Power, Dictionary Unpacking & **kwargs Explained

** in Python is one symbol with two unrelated jobs: between numbers it's the exponentiation operator (2 ** 3 == 8); in front of a dictionary or in a function signature, it means unpacking — spreading a dict's key-value pairs into arguments, or collecting arbitrary keyword arguments into one. Context determines which behavior you get, and conflating the two is a common source of confusion for developers moving past basic syntax.

Below is every context ** shows up in, with the operator precedence, return-type, and merging gotchas you need to use it correctly.

** as Exponentiation: Syntax and Precedence

Definition: base ** exponent raises base to the power of exponent and returns the result using Python's built-in arithmetic rules.

result = 2 ** 10
print(result)
# 1024 — standard exponentiation, int ** int returns int

The python power operator binds tighter than almost every other arithmetic operator, including unary minus. This trips people up constantly:

value = -2 ** 2
print(value)
# -4, not 4 — ** evaluates before unary minus is applied
# equivalent to -(2 ** 2)

value_correct = (-2) ** 2
print(value_correct)
# 4 — parentheses force the negation first

** is also right-associative, unlike most binary operators:

chained = 2 ** 3 ** 2
print(chained)
# 512, not 64 — evaluates as 2 ** (3 ** 2), i.e. 2 ** 9

Key takeaway: ** has higher precedence than unary minus and is right-associative, so -2 ** 2 and 2 ** 3 ** 2 rarely evaluate the way you'd guess at a glance.

** vs pow() vs math.pow(): Return Type Differences

Definition: All three compute exponentiation, but they differ in what types they accept and what type they return — and that difference matters more than the syntax choice.

** operatorpow() builtinmath.pow()Syntaxa ** bpow(a, b)math.pow(a, b)Return type (int, int)intintfloat (always)Supports moduloNoYes — pow(a, b, mod)NoAccepts negative exponentsYes (returns float)Yes (returns float)YesImport requiredNoNoimport mathprint(2 ** 3)          # 8 (int)
print(pow(2, 3))       # 8 (int)
print(math.pow(2, 3))  # 8.0 (float) — math.pow always returns float

print(2 ** -1)         # 0.5 — negative exponent forces float
print(pow(2, 3, 5))    # 3 — modular exponentiation: (2**3) % 5, unique to pow()

math.pow() always returns a float, even for clean integer results. If you need modular exponentiation (cryptography, hashing, competitive programming), pow(a, b, mod) is dramatically faster than a ** b followed by % mod, because it never builds the full intermediate value.

Key takeaway: Use ** or pow() to stay in int, use math.pow() only when you explicitly want a float, and use pow(a, b, mod) for modular exponentiation.

** for Dictionary Unpacking: The {**d1, **d2} Merge Pattern

Definition: Inside a dict literal, ** unpacks another dictionary's key-value pairs in place — this is the core mechanism behind dictionary unpacking in Python and the fastest built-in way to merge dicts.

defaults = {"theme": "light", "timeout": 30}
overrides = {"timeout": 60, "retries": 3}

merged = {**defaults, **overrides}
print(merged)
# {'theme': 'light', 'timeout': 60, 'retries': 3}
# later keys win — overrides.timeout (60) replaces defaults.timeout (30)

Order matters: in a conflict, whichever dict is unpacked last wins.

a = {"x": 1}
b = {"x": 2}
print({**a, **b})  # {'x': 2} — b overwrites a
print({**b, **a})  # {'x': 1} — a overwrites b

For Python 3.9+, the | merge operator does the same thing (defaults | overrides), but {**d1, **d2} remains the portable syntax that works on 3.5+.

Key takeaway: {**d1, **d2} merges dictionaries with last-unpacked-wins conflict resolution, and it's the most widely compatible merge syntax across Python 3 versions.

**kwargs in Function Definitions: Collecting Keyword Arguments

Definition: In a function signature, **kwargs collects any keyword arguments not explicitly named in the parameter list into a single dict — this is what python kwargs refers to.

def create_user(name, **kwargs):
   print(f"Name: {name}")
   print(f"Extra fields: {kwargs}")

create_user("Bob", age=25, role="editor")
# Name: Bob
# Extra fields: {'age': 25, 'role': 'editor'}
# kwargs is a plain dict containing every keyword arg not matched to a named parameter

kwargs is a convention, not a keyword — the mechanism is the ** prefix:

def log_event(event_name, **metadata):
   print(event_name, metadata)

log_event("purchase", user_id=42, amount=19.99)
# purchase {'user_id': 42, 'amount': 19.99}

**kwargs must come after *args and after any positional or keyword-only parameters — Python enforces this ordering with a SyntaxError before the function ever runs.

Key takeaway: **kwargs gives a function an open-ended way to accept keyword arguments it doesn't explicitly define, packaged as a regular dict inside the function body.

Passing **dict to a Function: Unpacking as Named Arguments

Definition: The inverse of **kwargs — calling a function with **some_dict unpacks that dict's keys as named arguments at the call site.

def connect(host, port, timeout=5):
   print(f"Connecting to {host}:{port}, timeout={timeout}")

config = {"host": "db.internal", "port": 5432, "timeout": 10}
connect(**config)
# Connecting to db.internal:5432, timeout=10
# each key in config is matched to a parameter name

The dict's keys must match the function's parameter names exactly, or you get a TypeError:

bad_config = {"host": "db.internal", "wrong_key": 5432}
connect(**bad_config)
# TypeError: connect() missing 1 required positional argument: 'port'

Key takeaway: **dict at a call site unpacks keys as named arguments, and every key must exactly match a parameter name or the call fails with a TypeError.

Nested ** Unpacking: Merging Multiple Dicts

Definition: You can chain more than two ** unpacks in a single literal or call, and Python resolves conflicts left to right.

base = {"env": "prod", "debug": False}
feature_flags = {"debug": True, "new_ui": True}
user_overrides = {"new_ui": False}

final_config = {**base, **feature_flags, **user_overrides}
print(final_config)
# {'env': 'prod', 'debug': True, 'new_ui': False}
# user_overrides.new_ui (False) wins because it's unpacked last

The same chaining works in function calls, but with a key difference — collisions there raise instead of silently overwriting:

def build(**kwargs):
   return kwargs

print(build(**{"a": 1}, **{"b": 2}))
# {'a': 1, 'b': 2} — fine, no overlapping keys

print(build(**{"a": 1}, **{"a": 99}))
# TypeError: build() got multiple values for keyword argument 'a'

Key takeaway: Chaining ** unpacks resolves key conflicts left-to-right in dict literals, but the identical conflict at a function call site raises a TypeError instead of silently overwriting.

Four Real-World Patterns

1. Config merging (defaults + environment + overrides)

DEFAULTS = {"log_level": "INFO", "retries": 3}
ENV_CONFIG = {"log_level": "DEBUG"}
runtime_config = {**DEFAULTS, **ENV_CONFIG}
# {'log_level': 'DEBUG', 'retries': 3} — environment wins over defaults

2. Function wrappers / decorators that pass through arbitrary arguments

def timed(func):
   def wrapper(*args, **kwargs):
       start = time.time()
       result = func(*args, **kwargs)
       print(f"{func.__name__} took {time.time() - start:.4f}s")
       return result
   return wrapper
# **kwargs here lets the wrapper support ANY wrapped function's signature

3. Building API request parameters from optional fields

def build_params(**filters):
   return {k: v for k, v in filters.items() if v is not None}

params = build_params(status="active", category=None, limit=50)
# {'status': 'active', 'limit': 50} — None values dropped before hitting the API

4. Updating dataclass-like objects immutably

from dataclasses import dataclass, asdict

@dataclass
class User:
   name: str
   role: str

original = User(name="Alice", role="viewer")
updated = User(**{**asdict(original), "role": "admin"})
# User(name='Alice', role='admin') — unpack, override one field, rebuild

Key takeaway: Config layering, pass-through wrappers, clean API params, and immutable object updates cover the vast majority of ** usage in real codebases.

FAQ

What does ** mean in Python?
** has two meanings depending on context: between two numbers it's the exponentiation operator (3 ** 2 == 9); before a dictionary or in a function signature it means unpacking, either spreading a dict's keys as arguments or collecting keyword arguments into a dict.

Is ** the same as pow() in Python?
Not exactly. ** and pow() both return an int for integer inputs and behave the same for basic exponentiation, but only pow() supports a third argument for modular exponentiation (pow(a, b, mod)), which ** cannot do.

Why does math.pow() return a float when ** returns an int?
math.pow() is a thin wrapper around the C standard library's pow() function, which always operates on and returns floating-point numbers, regardless of input type. ** and the pow() builtin preserve int results when both operands are integers.

How do you merge two dictionaries in Python using **?
Use {**dict1, **dict2} inside a new dict literal. Keys from dict2 overwrite matching keys from dict1 because it's unpacked second — the dict unpacked last always wins on conflicts.

What is the difference between *args and **kwargs?
*args collects extra positional arguments into a tuple; **kwargs collects extra keyword arguments into a dict. Both let a function accept an arbitrary number of arguments, but they capture different argument types and must appear in that order in a function signature (*args before **kwargs).

Can you unpack a dictionary into function arguments?
Yes. Calling my_function(**my_dict) unpacks each key in my_dict as a named argument, provided every key matches a parameter name in the function's signature exactly. A mismatched key raises a TypeError for an unexpected keyword argument.

Does ** have higher precedence than unary minus in Python?
Yes. -2 ** 2 evaluates to -4, not 4, because ** binds before the unary minus is applied — it's equivalent to -(2 ** 2). Use parentheses, (-2) ** 2, to force the negation first.

Learn what the double asterisk (**) means in Python, from exponentiation with the Python power operator to dictionary unpacking and **kwargs. This guide explains operator precedence, the differences between **, pow(), and math.pow(), how to merge dictionaries, and how to pass keyword arguments to functions. Explore practical examples and common pitfalls, including duplicate keys and argument conflicts.

Share this article
Get more articles
like this
Thank you! Your submission has been received!
Oops! Something went wrong.

Create a free account to access the full topic

Wide range of learning tracks for beginners and experienced developers
Study at your own pace with your personal study plan
Focus on practice and real-world experience
Andrei Maftei
It has all the necessary theory, lots of practice, and projects of different levels. I haven't skipped any of the 3000+ coding exercises.