Dictionary Data Type in Python: Complete Key-Value Pair Storage and Hash Table Implementation Guide

Dictionary Data Type in python

Dictionaries were the data structure that made me fall in love with Python. Coming from languages where I had to manually build hash maps or fight with verbose syntax, discovering how naturally {key: value} fits into the language felt like a genuine relief. Once I understood how dictionaries actually work internally as hash tables, I also stopped writing accidentally slow code without realizing it.

What Is a Dictionary?

A dictionary is an unordered (well, technically insertion-ordered since Python 3.7) collection of key-value pairs, where each key must be unique and hashable, and can map to any value at all.

student = {"name": "Ahmad", "age": 21, "field": "MBBS"}
print(student)
print(type(student))

Output:

{'name': 'Ahmad', 'age': 21, 'field': 'MBBS'}
(class 'dict')

Creating Dictionaries

There are several ways to construct a dictionary in Python.

# Literal syntax
d1 = {"a": 1, "b": 2}

# Using the dict() constructor
d2 = dict(a=1, b=2)

# From a list of tuples
d3 = dict([("a", 1), ("b", 2)])

# Dictionary comprehension
d4 = {x: x**2 for x in range(5)}

print(d1, d2, d3, d4)

Output:

{'a': 1, 'b': 2} {'a': 1, 'b': 2} {'a': 1, 'b': 2} {0: 0, 1: 1, 2: 4, 3: 9, 4: 16}

Accessing, Adding, and Updating Values

student = {"name": "Ahmad", "age": 21}

# Access
print(student["name"])

# Add a new key
student["city"] = "Lahore"

# Update an existing key
student["age"] = 22

print(student)

Output:

Ahmad
{'name': 'Ahmad', 'age': 22, 'city': 'Lahore'}

Accessing a key that doesn’t exist with square brackets raises a KeyError, so I almost always use .get() when I’m not certain a key is present.

print(student.get("country"))
print(student.get("country", "Unknown"))

Output:

None
Unknown

Removing Items

student = {"name": "Ahmad", "age": 21, "city": "Lahore"}

removed = student.pop("city")
print(removed)
print(student)

del student["age"]
print(student)

Output:

Lahore
{'name': 'Ahmad', 'age': 21}
{'name': 'Ahmad'}

How Dictionaries Work Internally: The Hash Table

This is the part that genuinely changed how I think about performance. CPython implements dictionaries as hash tables. When you insert a key-value pair, Python computes hash(key), and uses that hash value to determine which “slot” in an internal array the entry belongs to. When you later look up that key, Python recomputes its hash and jumps almost directly to the correct slot, rather than scanning through every item one by one.

key = "name"
print(hash(key))

Output (this specific value varies by Python version and run, since string hashing is randomized for security by default):

-2600834242705311581

Because lookups depend on jumping to a hash-derived slot rather than scanning sequentially, average-case dictionary lookups, insertions, and deletions run in O(1) time — constant time, regardless of how many items the dictionary holds. Compare that to searching for a value inside a list, which requires an O(n) linear scan in the worst case.

import timeit

my_list = list(range(100000))
my_dict = {i: True for i in range(100000)}

list_time = timeit.timeit(lambda: 99999 in my_list, number=1000)
dict_time = timeit.timeit(lambda: 99999 in my_dict, number=1000)

print(f"List lookup: {list_time:.5f}s")
print(f"Dict lookup: {dict_time:.5f}s")

Typical output:

List lookup: 0.85000s
Dict lookup: 0.00015s

The dictionary lookup is dramatically faster, and that gap only widens as the collection grows larger, precisely because of the hash table’s constant-time behavior versus the list’s linear scan.

Why Keys Must Be Hashable

Only immutable, hashable objects can serve as dictionary keys — strings, numbers, and tuples (as long as the tuple itself only contains hashable elements) all qualify. Lists and dictionaries themselves cannot be keys, because their contents can change, which would make their hash value unstable — and a hash table absolutely depends on a key’s hash never changing after insertion.

bad_dict = {[1, 2]: "value"}
TypeError: unhashable type: 'list'
good_dict = {(1, 2): "value"}
print(good_dict)

Output:

{(1, 2): 'value'}

Handling Hash Collisions

It’s worth understanding that two different keys can occasionally produce the same hash, or hashes that map to the same internal slot — this is called a collision. CPython resolves collisions using a technique called open addressing, where it probes subsequent slots in a defined sequence until it finds the correct one. This is handled entirely internally, but it’s part of why dictionary performance, while excellent on average, isn’t a hard mathematical guarantee in every single case — pathological collision patterns could in theory degrade performance, though this is extremely rare in practice with Python’s hashing algorithm.

Iterating Over a Dictionary

student = {"name": "Ahmad", "age": 21, "city": "Lahore"}

for key in student:
    print(key)

print("---")

for key, value in student.items():
    print(f"{key}: {value}")

Output:

name
age
city
---
name: Ahmad
age: 21
city: Lahore

Since Python 3.7, dictionaries officially guarantee insertion order is preserved during iteration — a behavior that used to be an implementation detail in earlier versions but is now a documented language feature.

Useful Dictionary Methods

student = {"name": "Ahmad", "age": 21}

print(student.keys())
print(student.values())
print(student.items())

student.update({"city": "Lahore", "age": 22})
print(student)

print(student.setdefault("country", "Pakistan"))
print(student)

Output:

dict_keys(['name', 'age'])
dict_values(['Ahmad', 21])
dict_items([('name', 'Ahmad'), ('age', 21)])
{'name': 'Ahmad', 'age': 22, 'city': 'Lahore'}
Pakistan
{'name': 'Ahmad', 'age': 22, 'city': 'Lahore', 'country': 'Pakistan'}

setdefault() is one I use a lot when grouping data — it returns the value if the key already exists, or inserts a default value and returns that.

Nested Dictionaries

Real-world data, especially anything coming from an API response, is often deeply nested.

patient = {
    "name": "Ahmad",
    "vitals": {
        "heart_rate": 72,
        "bp": "120/80"
    }
}

print(patient["vitals"]["heart_rate"])

Output:

72

Dictionary Comprehensions

Dictionary comprehensions let me transform data in a single readable line instead of a manual loop with .update() calls.

prices = {"apple": 100, "banana": 40, "cherry": 250}
discounted = {item: price * 0.9 for item, price in prices.items()}
print(discounted)

Output:

{'apple': 90.0, 'banana': 36.0, 'cherry': 225.0}

Real-World and Automation Use Cases

Dictionaries are everywhere in practical Python work. I use them constantly for:

  • Configuration management: storing settings loaded from a JSON or YAML file directly as a dictionary.
  • Counting and grouping: tallying word frequency, grouping records by category.
  • Caching / memoization: storing already-computed results keyed by their input arguments.
  • Representing JSON data: since json.loads() maps JSON objects directly to Python dictionaries.
import json

response = '{"status": "ok", "user": {"id": 1, "name": "Ahmad"}}'
data = json.loads(response)
print(data["user"]["name"])

Output:

Ahmad

Here’s a simple word-frequency counter, a very common automation pattern:

text = "the quick brown fox jumps over the lazy dog the fox runs"
counts = {}

for word in text.split():
    counts[word] = counts.get(word, 0) + 1

print(counts)

Output:

{'the': 3, 'quick': 1, 'brown': 1, 'fox': 2, 'jumps': 1, 'over': 1, 'lazy': 1, 'dog': 1, 'runs': 1}

This exact pattern also gets simplified with collections.Counter, which is built specifically for this use case:

from collections import Counter

text = "the quick brown fox jumps over the lazy dog the fox runs"
counts = Counter(text.split())
print(counts.most_common(2))

Output:

[('the', 3), ('fox', 2)]

Best Practices

  • Use .get() instead of [] when a key might not exist, to avoid handling KeyError exceptions manually.
  • Prefer dictionary comprehensions over manual loops for simple key-value transformations.
  • Use collections.defaultdict when you’re repeatedly building up grouped or nested data, to avoid manually checking whether a key already exists.
  • Avoid using mutable objects like lists as dictionary keys — it’s not just discouraged, it’s not allowed at all.
from collections import defaultdict

groups = defaultdict(list)
records = [("fruit", "apple"), ("veg", "carrot"), ("fruit", "banana")]

for category, item in records:
    groups[category].append(item)

print(dict(groups))

Output:

{'fruit': ['apple', 'banana'], 'veg': ['carrot']}

Common Mistakes

Modifying a dictionary’s keys while iterating over it directly raises a RuntimeError, which trips up a lot of people.

d = {"a": 1, "b": 2, "c": 3}
for key in d:
    if d[key] == 2:
        del d[key]
RuntimeError: dictionary changed size during iteration

The fix is to iterate over a copy of the keys instead:

d = {"a": 1, "b": 2, "c": 3}
for key in list(d.keys()):
    if d[key] == 2:
        del d[key]

print(d)

Output:

{'a': 1, 'c': 3}

FAQs

Are Python dictionaries ordered? Yes, since Python 3.7, insertion order is guaranteed and officially part of the language specification, not just an implementation detail.

What’s the time complexity of dictionary lookups? On average, O(1) — constant time — thanks to the underlying hash table implementation, though worst-case scenarios involving many hash collisions could theoretically be slower.

Can I use a tuple as a dictionary key but not a list? Yes. Tuples are immutable and hashable (as long as their contents are also hashable), while lists are mutable and therefore unhashable, so they cannot be used as keys.

What’s the difference between dict.get() and dict[key]? dict[key] raises a KeyError if the key doesn’t exist. dict.get(key) returns None (or a specified default) instead of raising an error.

When should I use defaultdict instead of a regular dictionary? When you’re repeatedly appending or building up values under keys that might not exist yet, since defaultdict avoids manual existence checks.

Summary

Dictionaries are Python’s built-in hash table implementation, giving average O(1) lookup, insertion, and deletion performance by mapping hashable keys to array slots derived from their hash values. That performance characteristic is exactly why dictionaries dominate so much of real-world Python code — configuration, JSON data, caching, counting, and grouping all lean on this same underlying mechanism. Understanding the hash table foundation underneath the simple {key: value} syntax made me both a faster and more deliberate Python programmer.

References

  • Python official documentation on dictionaries: https://docs.python.org/3/library/stdtypes.html#mapping-types-dict
  • Python tutorial on dictionaries: https://docs.python.org/3/tutorial/datastructures.html#dictionaries
  • collections module documentation: https://docs.python.org/3/library/collections.html
  • Python Time Complexity wiki (official): https://wiki.python.org/moin/TimeComplexity

Total
0
Shares

Leave a Reply

Previous Post
List Data Type in python

List Data Type in Python: Complete Mutable Sequence Creation and Manipulation Implementation Guide

Next Post
Tuple Data Type in python

Tuple Data Type in Python: Complete Immutable Sequence Creation and Operations Implementation Guide

Related Posts