Fun with Learning Technology
LearnCoursesQuestionsTracksToolsNewsExplorePractice
Fun with Learning Technology

A new problem, explained clearly, every day.

Subscribe
Learn
  • Lessons
  • Topics
  • News
  • Tools
  • Courses
  • Career tracks
  • Everything
Site
  • About
  • Contact
  • Support
  • Privacy
  • Terms
Get the daily one

One email per new problem. No spam.

Request a tutorial

Requests shape what gets made next.

© 2026 Fun with Learning TechnologyRSS
Home›Courses›Python›Dictionaries

Data Structures

Dictionaries

A dictionary is a mutable, unordered collection that stores data in key-value pairs using a highly efficient hash table implementation. It is essential for high-performance applications because it allows for near-instant data retrieval regardless of the collection's total size. Developers should reach for dictionaries whenever they need to map unique identifiers to specific information, such as tracking configurations, user records, or frequency counts.

The Core Mechanics of Hash Maps

At its architectural heart, a dictionary operates as a hash table. When you provide a key, Python computes a hash value for that key using an internal function. This integer hash acts as an index to find the exact memory slot where the corresponding value resides. Because this lookup process relies on mathematical indexing rather than scanning through items one by one, the time required to retrieve a value remains constant, regardless of whether the dictionary contains ten or ten million entries. This design is why we say dictionary lookups are O(1) in complexity. For this system to function, keys must be hashable; that is, they must be immutable objects like strings, integers, or tuples. If you attempted to use a mutable list as a key, the hash value could change if the list content changed, which would break the integrity of the storage. Understanding this hash-based approach explains why dictionary keys must be unique: if you assign a value to an existing key, you are simply overwriting the data at that specific pre-calculated memory slot.

# A dictionary stores data as key-value pairs
user_settings = {
    "theme": "dark",
    "notifications": True,
    "max_retries": 3
}

# Accessing a value by its unique key is an O(1) operation
print(user_settings["theme"])  # Output: dark

Handling Keys and Avoiding Errors

A common point of failure for new developers is assuming that a key will always exist in a dictionary. When you use the bracket syntax to access a key, Python raises a KeyError if that key is not found, which can crash your application. To write robust, professional code, you should anticipate these missing values. The .get() method is the standard tool for this scenario. It allows you to attempt to retrieve a value, and if the key is missing, it returns None by default or a custom fallback value that you specify. This approach is superior to using 'if key in dictionary' checks because it performs the lookup only once, making it more efficient and readable. By mastering safe retrieval patterns, you ensure your data processing pipelines are resilient against unexpected data inputs, such as missing configuration flags or incomplete API responses that might otherwise trigger runtime exceptions during production deployment.

# Accessing a key that doesn't exist triggers a KeyError
# Instead, use .get() for safer access
config = {"timeout": 30}

# Provide a default value if the key is missing
retry_limit = config.get("retries", 5) 
print(retry_limit)  # Output: 5

Dynamic Modification and Memory Management

Dictionaries are dynamic objects, meaning they grow or shrink in memory as your program executes. When you add a new key-value pair, Python checks the internal hash table capacity. If the table is nearly full, Python allocates a larger block of memory and re-hashes existing keys into the new, larger table. While this 'resize' operation is technically expensive, it occurs infrequently enough that the average time for adding an item remains amortized O(1). Because of this design, you can add or remove items on the fly by simply assigning a value to a new key or using the del keyword. When you delete a key, Python marks that memory slot as empty, allowing it to be reused later. Being conscious of this dynamic memory management is vital when dealing with massive datasets, as creating many dictionaries can consume significant system resources if not managed appropriately.

# Dictionaries can be modified dynamically
stats = {"clicks": 10}

# Adding a new key-value pair
stats["views"] = 100

# Deleting an existing key
del stats["clicks"]
print(stats)  # Output: {'views': 100}

Iterating Over Data Structures

Iteration over a dictionary provides flexible ways to interact with stored data. By default, iterating over a dictionary only yields its keys. However, most real-world scenarios require both the key and the associated value simultaneously. The .items() method is highly optimized for this purpose, providing an iterator that yields tuple pairs of (key, value). Using unpacking in a for-loop, such as 'for key, val in data.items()', is the standard, readable way to process dictionary contents. It is important to remember that since Python 3.7, dictionaries maintain insertion order, meaning you can rely on the order in which items were added to be the order in which they appear during iteration. This shift from unordered to ordered behavior makes dictionaries much more predictable when your logic depends on sequence, such as processing timestamps or ordered status updates during a session.

data = {"a": 1, "b": 2, "c": 3}

# Iterate through keys and values together efficiently
for key, value in data.items():
    print(f"Key: {key}, Value: {value}")

Advanced Techniques with Dictionary Comprehensions

Dictionary comprehensions provide a concise and syntactically expressive way to transform or filter data into a dictionary structure. They mirror list comprehensions but use curly braces and a colon to define the key-value relationship. This is the idiomatic way to map one collection to another—for example, converting a list of user objects into a dictionary keyed by user ID. Beyond mere mapping, you can include conditional logic inside the comprehension to filter entries on the fly, effectively performing an extraction and transformation task in a single line. Because the comprehension is executed inside Python's optimized loop machinery, it is often faster than writing out a standard for-loop with manual dictionary assignments. Mastering comprehensions elevates your code, making it less verbose and more declarative, which helps other engineers quickly grasp your intent when they review your implementation of data processing logic.

# Transform a list into a dictionary using comprehension
names = ["alice", "bob", "charlie"]

# Map names to their length
name_lengths = {name: len(name) for name in names if len(name) > 3}
print(name_lengths)  # Output: {'alice': 5, 'charlie': 7}

Key points

  • Dictionaries are implemented as hash tables to provide O(1) average time complexity for lookups.
  • Only immutable, hashable types like strings, integers, and tuples can function as keys.
  • The .get() method should be used to retrieve values safely without risking a KeyError.
  • Dictionary insertion order is guaranteed, making the structure predictable during iteration.
  • Dictionaries grow dynamically by re-hashing data into larger memory blocks when needed.
  • You can efficiently extract both keys and values using the .items() method in loops.
  • Dictionary comprehensions offer a declarative and performant syntax for data transformation.
  • Deleting keys removes them from the internal hash table and makes their slots available.

Common mistakes

  • Mistake: Accessing a missing key using bracket notation. Why it's wrong: Accessing a key that does not exist raises a KeyError and crashes the program. Fix: Use the .get() method or check membership with 'in'.
  • Mistake: Assuming dictionaries maintain order in all versions. Why it's wrong: Before Python 3.7, dictionary order was not guaranteed; relying on order for older versions causes logic bugs. Fix: Use collections.OrderedDict or rely on Python 3.7+ insertion order.
  • Mistake: Trying to use a list as a dictionary key. Why it's wrong: Dictionary keys must be hashable (immutable); lists are mutable and cannot be hashed. Fix: Convert the list to a tuple before using it as a key.
  • Mistake: Using dict.update() to merge and expecting a new dictionary returned. Why it's wrong: The update() method modifies the dictionary in-place and returns None, often leading to 'NoneType' errors. Fix: Use the merge operator (|) or create a copy before updating.
  • Mistake: Iterating over a dictionary and deleting keys simultaneously. Why it's wrong: Modifying the size of the collection during iteration raises a RuntimeError. Fix: Create a list of keys to delete first, then iterate over that list.

Interview questions

What is a dictionary in Python, and how does it differ from a list?

A dictionary in Python is a built-in data structure that stores data in key-value pairs, which is fundamentally different from a list. While a list is an ordered sequence of elements accessed by integer indices, a dictionary uses unique keys—which can be strings, numbers, or tuples—to map to specific values. This design allows for O(1) average time complexity for lookups, making dictionaries significantly more efficient than lists when you need to retrieve data based on a specific label or identifier rather than a numerical position.

How do you access a value in a dictionary, and what happens if the key does not exist?

You can access a value by placing the key inside square brackets, such as my_dict[key]. However, if that key is absent, Python raises a KeyError, which can crash your program. To prevent this, you should use the .get() method, which returns None or a specified default value instead of an error. For example, my_dict.get('age', 0) returns 0 if 'age' is missing, allowing your code to handle missing data gracefully without explicit exception handling.

Explain the difference between using .items() and .keys() when iterating over a dictionary.

When you iterate over a dictionary, using .keys() gives you access only to the keys, which is useful if you only need the identifiers. Conversely, .items() returns view objects containing both the key and the value as tuples, such as (key, value). Using .items() is generally more efficient and readable when you need to perform logic that depends on both parts of the pair, as it avoids the need to perform a second lookup inside the loop.

Compare the performance and utility of using a standard dictionary versus a collections.defaultdict.

A standard dictionary requires you to manually check if a key exists before appending to a list or incrementing a count, often requiring an if-else block. A collections.defaultdict is a subclass that solves this by providing a default factory function, such as list or int. This simplifies code, as it automatically initializes the value if the key is missing. While both offer similar O(1) performance, defaultdict is superior for frequency counting or grouping tasks, as it eliminates the overhead and boilerplate of checking for key existence every time.

Why must dictionary keys be immutable, and what happens if you attempt to use a list as a key?

Dictionary keys must be hashable, which means they must be immutable objects like strings, integers, or tuples containing immutable elements. This requirement exists because Python uses a hash table to store dictionary keys; the hash value of a key must remain constant throughout its lifecycle to ensure the mapping can be located later. If you use a list, which is mutable, its hash could change if the content changes, effectively breaking the dictionary lookup mechanism. Therefore, Python raises a TypeError if you try to use a list as a key.

How would you merge two dictionaries in Python 3.9+ versus older versions, and what are the trade-offs?

In Python 3.9 and newer, you can use the union operator | to merge two dictionaries, resulting in a new dictionary containing keys from both, where the second dict overwrites the first in case of key collisions. In older versions, you had to use the .update() method or a dictionary comprehension. The | operator is more readable and functional, as it doesn't mutate the original inputs. Using .update() is slightly faster if you already have the destination dictionary created, but it changes the original object, which can lead to unintended side effects if that dictionary is referenced elsewhere in your application.

All Python interview questions →

Check yourself

1. Which of the following is a valid way to create a dictionary in Python?

  • A.d = {1, 2, 3}
  • B.d = {'a': 1, 'b': 2}
  • C.d = (('a', 1), ('b', 2))
  • D.d = ['a': 1, 'b': 2]
Show answer

B. d = {'a': 1, 'b': 2}
Option 1 creates a set, not a dictionary. Option 2 uses the correct curly brace syntax with key-value pairs. Option 3 creates a tuple of tuples. Option 4 uses square brackets incorrectly. Dictionaries require key:value syntax within braces.

2. What is the result of calling d.get('x', 0) on a dictionary d that does not contain the key 'x'?

  • A.It raises a KeyError
  • B.It returns None
  • C.It returns 0
  • D.It adds the key 'x' with value 0 to the dictionary
Show answer

C. It returns 0
The get() method returns the provided default value (0) if the key is not found. It does not raise an error, does not return None if a default is provided, and does not modify the dictionary.

3. Why can you not use a list as a dictionary key?

  • A.Because lists are too large
  • B.Because lists do not have a length
  • C.Because lists are mutable and therefore unhashable
  • D.Because lists are ordered
Show answer

C. Because lists are mutable and therefore unhashable
Dictionary keys must be immutable so that their hash value remains constant. Since lists can be changed (mutated), they are unhashable. The other options are incorrect as lists have length and ordering.

4. What happens when you use the dictionary unpacking operator **d in a function call?

  • A.It passes the dictionary as a single keyword argument
  • B.It passes the keys and values as keyword arguments
  • C.It raises a TypeError
  • D.It converts the dictionary into a list of tuples
Show answer

B. It passes the keys and values as keyword arguments
The ** operator unpacks the dictionary into individual keyword arguments, where the key names become the parameter names. It does not pass the dictionary as one object or raise an error.

5. What is the most efficient way to check if a specific key exists in a dictionary?

  • A.Using 'if key in d.keys():'
  • B.Using 'try-except' block
  • C.Using 'if key in d:'
  • D.Using 'if d.get(key) is not None:'
Show answer

C. Using 'if key in d:'
Using 'if key in d' is the standard, most idiomatic, and efficient way to check for key existence. 'd.keys()' creates an unnecessary view object, 'try-except' is overhead-heavy, and 'get()' check fails if the key exists but its value is None.

Take the full Python quiz →

← PreviousSets and Set OperationsNext →Dictionary Methods and Nested Dicts

Python

78 lessons, free to read.

All lessons →

Track your progress

Sign in to mark lessons done, score quizzes and keep notes.

Open in the app