Back to Blog
Python

Python Pass Dictionary to Function: Syntax and Behavior

python pass dictionary to function: Learn how to pass a dictionary to a Python function, understand reference vs copy semantics, and avoid common pitfalls like mutable...

function argumentsdictionarykwargsmutable defaultsreference semantics
Illustration of a Python dictionary being passed into a function, with a copy and reference concept.

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

When you need to pass a dictionary to a function in Python, the language gives you several syntactically distinct options. The choice affects readability, flexibility, and whether the function can modify the caller's dictionary. Understanding how Python treats dictionary arguments is essential because dictionaries are mutable objects, and the function receives a reference, not a copy, unless you explicitly copy them.

How Python Passes Dictionary Arguments

Python uses pass-by-object-reference semantics for all function arguments. When you pass a dictionary, the function receives a reference to the same dictionary object. Any in-place modification inside the function affects the original dictionary. Consider this minimal example:

def add_item(data): data["new_key"] = 1 original = {"existing": 2} add_item(original) print(original) # {'existing': 2, 'new_key': 1}

The function did not return anything, yet original changed. This behavior is expected for mutable types, but it can surprise developers coming from value-semantics languages. If you need the function to work on a copy, you must explicitly copy the dictionary before passing it or inside the function.

Passing a Dictionary as a Single Argument

The simplest pattern is to declare a parameter that accepts the dictionary directly. This is appropriate when the dictionary is a cohesive data structure, such as a configuration object or a set of options.

def apply_config(config): timeout = config.get("timeout", 30) retries = config.get("retries", 3) # ... use values settings = {"timeout": 10, "retries": 5} apply_config(settings)

This approach keeps the function signature explicit about expecting a dictionary. It also allows the caller to pass any dict-like mapping, as long as it supports the methods you use, such as .get(). If the function only reads the dictionary, there is no risk of mutation. If it writes, you must decide whether that is intentional.

Using **kwargs to Expand a Dictionary

The **kwargs syntax unpacks a dictionary into named keyword arguments. This is useful when the function has a fixed set of named parameters and you want to pass a dictionary that maps parameter names to values.

def connect(host, port, timeout=30): print(host, port, timeout) params = {"host": "example.com", "port": 443, "timeout": 15} connect(**params)

This works only if the dictionary keys match the parameter names exactly. Extra keys cause a TypeError. Conversely, missing required parameters also cause an error. This pattern is common when forwarding arguments to another function, especially in decorators or wrapper functions.

A related pattern is to define a function that accepts arbitrary keyword arguments and collects them into a dictionary:

def log_event(**kwargs): for key, value in kwargs.items(): print(f"{key}: {value}") log_event(event="start", user_id=42)

Here the dictionary is created from the keyword arguments, which is the inverse of unpacking. The two patterns are complementary: **kwargs in a function definition collects keyword arguments into a dict, while **dict in a call unpacks a dict into keyword arguments.

Default Arguments and Mutable Dictionaries

A common mistake is using a dictionary as a default argument value. Python evaluates default arguments once at function definition time, not on each call. If the default is a mutable object like a dictionary, all calls that rely on the default share the same object.

def add_to_list(item, cache={}): cache[item] = True return cache print(add_to_list("a")) # {'a': True} print(add_to_list("b")) # {'a': True, 'b': True}

The second call sees the mutation from the first call. This is rarely the intended behavior. The standard fix is to use None as the default and create a fresh dictionary inside the function:

def add_to_list(item, cache=None): if cache is None: cache = {} cache[item] = True return cache

This pattern gives you a new dictionary on every call unless the caller explicitly passes one. It avoids the shared-state bug and is the recommended approach for any mutable default.

Avoiding Unintended Mutation with Copies

When you want the function to modify a dictionary without affecting the original, you can copy the dictionary before passing it or inside the function. Python provides two copy methods: a shallow copy via dict.copy() or copy.copy(), and a deep copy via copy.deepcopy().

import copy def normalize(data): data = data.copy() # shallow copy data["status"] = "normalized" return data original = {"values": [1, 2, 3]} result = normalize(original) print(original) # unchanged print(result) # has new key

A shallow copy is sufficient when the dictionary values are immutable or when you only replace top-level keys. If the values themselves are mutable and you modify them in place, the shallow copy still shares those nested objects. For example, if normalize did data["values"].append(4), the original list would also change because both dictionaries reference the same list. In that case, use copy.deepcopy() to create an independent structure.

Performance and Memory Considerations

Passing a dictionary itself is cheap because it only copies a reference, not the entire data structure. The cost comes when you copy the dictionary to avoid mutation. A shallow copy of a large dictionary copies all keys and values references, which is O(n) in the number of entries. A deep copy is significantly more expensive because it recursively copies every nested object.

If you are working with large dictionaries and only need to protect against top-level changes, a shallow copy is usually sufficient. If you need full isolation, measure whether the deep copy cost is acceptable. In performance-sensitive code, consider designing the function to not mutate the input at all, or to return a new dictionary instead of copying defensively.

Another performance consideration is the use of **kwargs. Unpacking a dictionary into keyword arguments creates a new call frame and may be slightly slower than passing the dictionary directly, but the difference is negligible for most applications. The real cost is in the overhead of argument binding, not the unpacking itself. Prefer readability and correctness over micro-optimizations unless profiling shows a bottleneck.

Choosing the Right Approach

The decision between passing a dictionary directly, using **kwargs, or copying depends on the function's contract and how the caller intends to use it.

ApproachUse whenMutation behavior
Single dict parameterThe dictionary is a cohesive unit; keys are dynamic or unknownFunction receives reference; may mutate if written
**kwargs in callFunction has named parameters; dictionary keys match parameter namesNo dictionary object is passed; values are bound to parameters
**kwargs in definitionFunction collects arbitrary keyword argumentsNew dictionary created from call arguments
Copy before passingFunction must not affect original; only top-level changesOriginal untouched; copy may share nested objects

Use a single dictionary parameter when the function needs to access keys dynamically or when the dictionary represents an options bag. Use **kwargs when you are forwarding arguments to another function or when the function signature benefits from explicit parameter names. Copy the dictionary only when you must prevent side effects; otherwise, document that the function may mutate its input.

Handling Nested Dictionaries and Deep Copy

Nested dictionaries introduce a subtlety: a shallow copy still shares the inner dictionaries. If you need to modify nested structures without affecting the original, you must deep copy. The copy module provides deepcopy, which handles cycles and complex objects correctly.

import copy def update_nested(data): data = copy.deepcopy(data) data["user"]["name"] = "Alice" return data original = {"user": {"name": "Bob", "age": 30}} result = update_nested(original) print(original["user"]["name"]) # Bob print(result["user"]["name"]) # Alice

Deep copy is recursive and can be expensive for large or deeply nested structures. It also copies objects that may not be copyable, such as file handles or database connections. In such cases, you may need a custom copy strategy or a design that avoids copying altogether. Consider whether the function can operate on a read-only view or return a new structure instead of mutating the input.

Compatibility with Typed Dictionaries

Modern Python supports type hints for dictionaries, such as dict[str, int] or TypedDict from typing. When you pass a dictionary to a function, the type hint communicates the expected key and value types. This is especially useful in large codebases where static type checkers can catch mismatches.

from typing import TypedDict class User(TypedDict): name: str age: int def greet(user: User) -> str: return f"Hello, {user['name']}" user = {"name": "Alice", "age": 30} greet(user)

TypedDict does not enforce runtime behavior; it is purely for static analysis. The function still receives a regular dictionary. However, using type hints makes the expected structure explicit and helps prevent accidental key typos. When using **kwargs, type hints are less precise because the dictionary keys are not statically known. For functions that accept a fixed set of keyword arguments, consider using **kwargs with a TypedDict as the type annotation, though this is not yet fully supported by all type checkers.

python pass dictionary to function: Practical Usage and Code | RYUSLOG DEV