Python Dictionaries
Dictionaries are mutable, unordered collections of key-value pairs. They are one of Python’s most powerful data structures, offering fast lookups, insertions, and deletions. Understanding their internal mechanics, performance characteristics, and advanced usage is crucial for writing high-performance, clean Python code.
1. Dictionary Internals: How Python Stores Data
- Dictionaries are implemented as hash tables, similar to sets.
- Each key is hashed to determine its position in memory.
- Values are stored as pointers to objects, enabling efficient storage of large or complex objects.
- Python dictionaries are dynamic: they resize automatically as elements are added.
d = {"a": 1, "b": 2}
print(hash("a")) # Hash value used internally
Key Insights:
- Keys must be hashable (immutable types like str, int, tuple).
- Values can be any type, mutable or immutable.
- Average-case complexity for lookups, insertions, and deletions: O(1).
2. Creating Dictionaries
A dictionary in Python is a built-in data structure that stores data in key–value pairs.
It provides a powerful and efficient way to organize, retrieve, and modify data based on unique keys rather than numerical indexes (as in lists or tuples).
Dictionaries are unordered (before Python 3.7) and insertion-ordered (from Python 3.7+), mutable, and dynamic, meaning you can change their contents at any time by adding, updating, or removing key–value pairs.
There are multiple ways to create dictionaries in Python, depending on the situation and coding style.
A. Dictionary Literals (Using Curly Braces)
The most direct and common way to create a dictionary is by using curly braces {} with key–value pairs separated by colons (:).
person = {"name": "Alice", "age": 25, "profession": "Engineer"}
Here:
- "name", "age", and "profession" are keys (must be immutable types such as strings, numbers, or tuples).
- Their corresponding values ("Alice", 25, "Engineer") can be of any type — strings, numbers, lists, even other dictionaries.
Each key must be unique; if a duplicate key appears, Python will overwrite the previous value.
d = {"x": 1, "x": 2}
print(d) # {'x': 2}
This literal syntax is the most readable and widely used way to define dictionaries in everyday programming.
B. Using the dict() Constructor
Dictionaries can also be created using the built-in dict() constructor, which allows you to define key–value pairs as keyword arguments.
person2 = dict(name="Bob", age=30)
This syntax is concise and clean, especially when keys are valid Python identifiers (i.e., strings without spaces or special characters).
The constructor automatically creates string keys from the provided identifiers.
For example:
print(person2) # {'name': 'Bob', 'age': 30}
This method is particularly convenient when you’re creating dictionaries from variable-like data or working with function arguments.
C. From a List (or Tuple) of Pairs
Another flexible way to build a dictionary is by passing a list (or any iterable) of key–value pairs (usually as tuples) to the dict() constructor.
pairs = [("x", 1), ("y", 2)]
d = dict(pairs)
This method is useful when:
- You’re converting structured data (like rows from a CSV file or database results) into a dictionary.
- You’re dynamically generating keys and values in pairs.
The constructor automatically assigns the first element of each tuple as the key and the second as the value.
It also works with lists of lists or any other iterable form:
d = dict([["a", 10], ["b", 20]])
print(d) # {'a': 10, 'b': 20}
This approach provides flexibility when working with external data or transformations.
D. Creating an Empty Dictionary
An empty dictionary is created using empty curly braces {} or dict() with no arguments.
empty = {}
This initializes a blank dictionary, ready to store new key–value pairs.
You can then populate it dynamically:
empty["language"] = "Python"
empty["version"] = 3.12
Empty dictionaries are commonly used when collecting or aggregating data in loops or from user input.
Important Notes
- Keys must be immutable types: strings, numbers, or tuples containing only immutable elements. Lists or sets cannot be used as keys.
- Values can be any data type, including other dictionaries, lists, or even functions.
- Dictionary literals are faster than calling dict() — which matters in performance-sensitive applications.
- From Python 3.7 onwards, dictionaries preserve the insertion order of keys. This means that items will appear in the order they were added.
3. Accessing, Adding, and Updating Elements
Once a dictionary is created, you can easily retrieve, insert, update, or remove its elements.
Dictionaries provide flexible and efficient ways to manage key–value data, making them ideal for handling structured and dynamic information.
Let’s look at how each of these operations works in detail.
Accessing Elements
Dictionary elements are accessed using their keys, not by position (since dictionaries don’t use numeric indexes like lists).
The most common way to access a value is through square brackets ([]):
person = {"name": "Alice", "age": 25, "profession": "Engineer"}
print(person["name"]) # 'Alice'
Here, the key "name" is used to retrieve its corresponding value 'Alice'.
However, if you try to access a key that doesn’t exist, Python raises a KeyError:
print(person["location"]) # ❌ KeyError
To avoid this, you can use the get() method, which is a safer alternative.
print(person.get("age")) # 25
print(person.get("location")) # None
print(person.get("location", "Unknown")) # 'Unknown'
The get() method returns:
- The value if the key exists.
- A default value (e.g., "Unknown") if the key doesn’t exist.
- None if no default value is provided.
This makes get() extremely useful in production environments, where missing keys are common and should not interrupt program flow.
Pro Tip:
Always prefer get() when accessing optional keys to prevent runtime errors.
Adding and Updating Elements
Dictionaries are mutable, so you can add new key–value pairs or modify existing ones using the same syntax.
Adding a new element:
person["location"] = "Delhi"
print(person)
# {'name': 'Alice', 'age': 25, 'profession': 'Engineer', 'location': 'Delhi'}
If the key doesn’t exist, Python automatically adds it to the dictionary.
Updating an existing element:
person["age"] = 26
print(person)
# {'name': 'Alice', 'age': 26, 'profession': 'Engineer', 'location': 'Delhi'}
Here, the existing key "age" is updated to a new value.
This same syntax works seamlessly for both addition and modification — Python handles it based on whether the key already exists.
You can also update multiple entries at once using the update() method:
person.update({"age": 27, "profession": "Architect"})
print(person)
# {'name': 'Alice', 'age': 27, 'profession': 'Architect', 'location': 'Delhi'}
The update() method can take another dictionary, or even an iterable of key–value pairs.
It’s often used when merging or refreshing data from another source.
Deleting Elements
Dictionaries provide several ways to remove key–value pairs, depending on your use case.
Using del statement:
del person["profession"]
print(person)
# {'name': 'Alice', 'age': 27, 'location': 'Delhi'}
This completely deletes the specified key and its value.
If the key doesn’t exist, it raises a KeyError.
Using pop() method:
person.pop("location")
print(person)
# {'name': 'Alice', 'age': 27}
pop() removes a key and returns its value — a useful feature if you need the removed item for further processing.
You can also provide a default value to pop() to avoid errors if the key doesn’t exist:
person.pop("salary", "Not Found")
This makes pop() safer for uncertain data scenarios.
Summary Table
| Operation | Method / Syntax | Behavior |
|---|---|---|
| Access | person["key"] | Returns value if key exists, else raises KeyError. |
| Safe Access | person.get("key", default) | Returns value or default (no error). |
| Add | person["new_key"] = value | Adds new key–value pair. |
| Update | person["existing_key"] = new_value | Updates existing key. |
| Batch Update | person.update({...}) | Updates multiple keys at once. |
| Delete | del person["key"] | Removes key–value pair (error if key missing). |
| Pop | person.pop("key", default) | Removes key and returns value. |
4. Dictionary Iteration — Advanced Techniques
Iterating through dictionaries is one of the most common operations in Python programming — whether you’re analyzing data, transforming structures, or building algorithms.
Python offers multiple efficient and expressive ways to traverse dictionary elements, each serving a different purpose.
Let’s explore the most Pythonic ways to iterate through dictionaries effectively.
Iterating Over Keys
When you loop directly over a dictionary, Python automatically iterates through its keys.
person = {"name": "Alice", "age": 25, "profession": "Engineer"}
for key in person:
print(key, person[key])
This approach gives you access to each key, and you can retrieve the corresponding value using person[key].
Under the hood, this is equivalent to:
for key in person.keys():
print(key, person[key])
Although both methods work the same way, looping directly over the dictionary is slightly more concise and equally efficient.
When to use:
Use this method when you need both the key and its corresponding value but don’t mind performing a lookup for each key.
Iterating Over Keys and Values
For optimal efficiency and cleaner code, Python provides the .items() method, which returns an iterable of key–value pairs (as tuples).
for key, value in person.items():
print(key, value)
This eliminates the need for multiple lookups because both the key and its associated value are retrieved together in a single step.
Behind the scenes, .items() returns a dictionary view object, which is lightweight and dynamically reflects any changes made to the dictionary during iteration.
Performance Note:
Using .items() is faster and more memory-efficient than accessing person[key] inside a loop, since it avoids repeated key lookups.
Iterating Over Values Only
If you only need to process the values, you can use the .values() method.
for value in person.values():
print(value)
This returns a view of all the values in the dictionary without exposing the keys.
It’s particularly useful for aggregate operations (like summing, filtering, or type checking).
For example:
ages = {"Alice": 25, "Bob": 30, "Charlie": 28}
total_age = sum(ages.values())
print(total_age) # 83
Dictionary Comprehension
Just like list comprehensions, Python supports dictionary comprehensions — a concise and elegant way to construct new dictionaries during iteration.
squared = {x: x**2 for x in range(5)}
print(squared) # {0: 0, 1: 1, 2: 4, 3: 9, 4: 16}
In this example:
- Each element from range(5) becomes a key.
- Its square (x**2) becomes the value.
Dictionary comprehensions are ideal for transformations, filtering data, or building lookup tables efficiently in one line.
Example with filtering:
even_squares = {x: x**2 for x in range(10) if x % 2 == 0}
print(even_squares)
# {0: 0, 2: 4, 4: 16, 6: 36, 8: 64}
Insertion Order Preservation (Python 3.7+)
Starting with Python 3.7, dictionaries maintain the insertion order of keys by default.
This means that when you iterate over a dictionary, the keys (and their corresponding values) appear in the same order in which they were added.
info = {}
info["first"] = 1
info["second"] = 2
info["third"] = 3
print(list(info.keys())) # ['first', 'second', 'third']
This behavior is now officially guaranteed by the language specification (from Python 3.7 onward) and is implemented using ordered hash tables internally.
The preserved order makes dictionary iteration predictable and consistent, especially useful for serialization, JSON operations, or displaying structured data.
Efficiency Tip
Use .items() when you need both keys and values during iteration.
It’s more efficient than accessing person[key] inside a loop, as it avoids redundant dictionary lookups.
Also, since dictionary views (.keys(), .values(), .items()) are dynamic, they reflect any updates made to the dictionary during iteration — a useful property in real-time data scenarios.
5. Nested Dictionaries
A nested dictionary is a dictionary that contains one or more dictionaries as its values.
This structure allows you to represent hierarchical or multi-level data, making it ideal for complex data modeling — similar to how JSON objects work.
In essence, a nested dictionary lets you group related information together in layers, giving your data both structure and context.
Creating a Nested Dictionary
A nested dictionary is created by placing one dictionary inside another.
Each outer key maps to another dictionary, which in turn contains inner keys and values.
students = {
"Alice": {"age": 25, "grade": "A"},
"Bob": {"age": 22, "grade": "B"}
}
Here:
- The outer dictionary stores student names ("Alice", "Bob") as keys.
- Each value is itself a dictionary holding details such as "age" and "grade".
This structure helps group each student’s data in an organized way, making it clear and easy to access.
Accessing Nested Data
To access data within a nested dictionary, you use multiple keys in succession — one for each level of nesting.
students["Alice"]["grade"] # 'A'
Here’s what happens step-by-step:
- students["Alice"] retrieves the inner dictionary {"age": 25, "grade": "A"}.
- From that dictionary, ["grade"] fetches the value "A".
If you try to access a key that doesn’t exist at any level, Python will raise a KeyError, so nested access often requires caution (or safer methods like get()).
Example:
students.get("Charlie", {}).get("grade", "Not Found") # 'Not Found'
Using get() like this prevents errors and allows you to specify defaults at each level.
Updating Nested Data
You can modify nested data just like any normal dictionary — simply specify both the outer and inner keys.
students["Bob"]["grade"] = "A+"
print(students)
# {'Alice': {'age': 25, 'grade': 'A'}, 'Bob': {'age': 22, 'grade': 'A+'}}
If the key already exists, its value is updated; otherwise, a new key–value pair is added to the inner dictionary.
You can also add entirely new inner dictionaries dynamically:
students["Charlie"] = {"age": 23, "grade": "B+"}
Now students includes a third record — all without altering the structure of existing entries.
Why Use Nested Dictionaries
Nested dictionaries are particularly useful for representing structured or relational data such as:
- User profiles — where each user has multiple attributes.
- Configuration files — with settings grouped under sections.
- API responses / JSON data — which naturally use nested structures.
- Hierarchical relationships — such as organizations, product catalogs, or datasets.
They allow clean and logical grouping, reducing data redundancy while improving readability.
Advanced Usage — Dynamic Nested Dictionaries
Sometimes, you may not know the nested keys in advance.
Manually initializing each level can be cumbersome — and that’s where defaultdict from the collections module becomes powerful.
from collections import defaultdict
nested = defaultdict(lambda: defaultdict(int))
nested["a"]["b"] += 1
print(nested)
# defaultdict( at ...>, {'a': defaultdict(, {'b': 1})})
Here’s how it works:
- The outer defaultdict automatically creates an inner defaultdict(int) whenever a new key is accessed.
- The inner dictionary uses int as its default factory, initializing missing values as 0.
- This allows operations like += 1 to work seamlessly without manual initialization.
This pattern is commonly used in:
- Counting frequencies, graph representations, or multi-level grouping.
- Scenarios involving incremental data construction where keys are discovered dynamically.
Nested Dictionaries and JSON
Because Python dictionaries mirror the structure of JSON objects, nested dictionaries are perfect for working with JSON data.
For example, when loading JSON from an API or file:
import json
data = json.loads('{"user": {"name": "Alice", "city": "Delhi"}}')
print(data["user"]["name"]) # Alice
You can treat it exactly like a nested dictionary.
This interoperability is one of the reasons dictionaries are so central to Python’s data ecosystem.
6. Dictionary Methods — Full Overview
| Method | Description | Complexity |
|---|---|---|
| keys() | Returns dict_keys view | O(1) |
| values() | Returns dict_values view | O(1) |
| items() | Returns dict_items view | O(1) |
| get(k, default) | Access with default | O(1) |
| pop(k) | Remove and return value | O(1) |
| popitem() | Remove last inserted item | O(1) |
| update() | Merge another dict | O(n) |
| clear() | Remove all items | O(n) |
| copy() | Shallow copy | O(n) |
Pro Tip: popitem() is handy for LIFO queue implementations using dictionaries.
7. Time Complexity Insights
| Operation | Average Case | Notes |
|---|---|---|
| Access | O(1) | Via key hash |
| Insertion | O(1) | May trigger resize occasionally |
| Deletion | O(1) | Hash lookup + remove |
| Iteration | O(n) | n = number of items |
| Copy | O(n) | Shallow copy |
| Update/Merge | O(m) | m = size of dict being merged |
Memory Note:
Dictionaries slightly overallocate to reduce hash collisions and maintain O(1) access.
8. Advanced Pythonic Techniques
Python’s dictionaries are incredibly powerful — and beyond the basics, they support several Pythonic shortcuts and advanced patterns that make code cleaner, faster, and more expressive.
These techniques are widely used in production code, data pipelines, and modern Python frameworks.
A. Multiple Assignments and Unpacking
Dictionaries integrate seamlessly with Python’s iterable unpacking syntax.
You can extract keys and values directly using tuple unpacking in a single line.
key, value = next(iter(person.items()))
Explanation:
- person.items() returns a view of (key, value) pairs.
- iter() creates an iterator over that view.
- next() retrieves the first item.
- Finally, the unpacking key, value = ... splits the tuple into individual variables.
This technique is cleaner and faster than manually indexing or looping, especially when you only need one key–value pair.
Use Case Example:
Extracting the first configuration, first record, or performing quick checks in small dictionaries.
B. Dictionary Comprehension with Conditions
Just like list comprehensions, dictionary comprehensions allow you to build new dictionaries in a concise and readable way — even with conditions.
even_squares = {x: x**2 for x in range(10) if x % 2 == 0}
How it works:
- The loop iterates over numbers 0–9.
- The condition if x % 2 == 0 filters only even numbers.
- Each even number becomes a key, and its square becomes the value.
Result:
{0: 0, 2: 4, 4: 16, 6: 36, 8: 64}
Why it’s powerful:
- Compact alternative to loops.
- Expressive and highly readable.
- Perfect for data transformations, filtering, and mapping operations in one line.
Advanced Tip:
You can even nest comprehensions or call functions inside them for more complex logic.
C. Dynamic Nesting with defaultdict
Manually creating nested dictionaries can be cumbersome, especially when the depth isn’t fixed.
Python’s defaultdict from the collections module lets you auto-create nested levels dynamically.
from collections import defaultdict
tree = lambda: defaultdict(tree)
data = tree()
data['root']['child']['leaf'] = 1
Explanation:
- The tree lambda defines a recursive defaultdict that creates another defaultdict(tree) for every new key.
- When data['root']['child']['leaf'] is assigned, each level ('root', 'child', 'leaf') is automatically created on the fly.
Advantages:
- No need for pre-initialization.
- Perfect for hierarchical data, such as trees, XML/JSON parsing, or graph structures.
- Reduces boilerplate and eliminates key errors.
Example Use Case:
Dynamic JSON construction, hierarchical logs, or machine learning feature grouping.
D. Using Tuples as Dictionary Keys
Since tuples are immutable and hashable, they can safely be used as dictionary keys — unlike lists or sets.
coords = {}
coords[(0,0)] = "origin"
coords[(1,2)] = "point A"
Why this works:
- The tuple (1, 2) represents a fixed coordinate pair.
- Dictionaries use hash values internally, and tuples provide stable hashes.
This pattern is common in:
- Mathematical computations (coordinates, vectors, matrices)
- Grids, games, or simulations
- Caching or memoization, where combinations of values are keys
Example:
def cache_key(a, b):
return (a, b)
Tuples make keys compact and expressive, providing multidimensional mapping in one step.
E. Merging Dictionaries (Python 3.9+)
From Python 3.9 onward, you can merge dictionaries using the | (pipe) operator — a modern and cleaner approach compared to update() or unpacking syntax.
d1 = {'a': 1}
d2 = {'b': 2}
merged = d1 | d2
Result:
{'a': 1, 'b': 2}
Explanation:
- The operator | creates a new dictionary containing the union of both.
- If both have the same key, the right-hand dictionary’s value overrides the left.
If you want to update in place, use the in-place variant |=:
d1 |= {'c': 3}
# d1 is now {'a': 1, 'c': 3}
Why it’s useful:
- Cleaner and more readable than older merge patterns.
- Especially helpful when combining configurations, API responses, or default parameters dynamically.
9. Real-World Applications
- Counting frequencies
words = ["apple","banana","apple"]
freq = {}
for w in words:
freq[w] = freq.get(w,0)+1
- Caching / Memoization
cache = {}
def fib(n):
if n in cache: return cache[n]
if n < 2: return n
cache[n] = fib(n-1)+fib(n-2)
return cache[n]
- Configuration / JSON-like data
config = {
"database": {"host":"localhost", "port":3306},
"api_keys": {"service_a":"key123"}
}
- Graph representation
graph = {
"A": ["B","C"],
"B": ["A","D"]
}
10. Best Practices
- Prefer get() over direct key access to avoid KeyError.
- Use defaultdict for dynamic nested structures.
- Use tuples as keys for immutable multi-value keys.
- Merge dictionaries with | (Python 3.9+) for readability.
- Remember dicts maintain insertion order, but avoid relying on order for logic unless intentional.
Conclusion
Python dictionaries are extremely versatile and performance-optimized. By understanding their hash table mechanics, memory behavior, nested structures, and advanced usage patterns, you can leverage dictionaries to build efficient, clean, and scalable applications — from caching and counting to complex data structures like graphs and trees.
