Answer
The core idea
Swift's Dictionary is a value type that stores key-value pairs, where each unique key identifies an associated value. Unlike an Array, which accesses values by position, a dictionary finds values by key. Under the hood, it uses a hash table, which provides O(1) average-case lookup, insertion, and removal.
1. What is a Dictionary in Swift?
A Dictionary stores values by key.
For example, users could be stored by their ID:
var users: [Int: String] = [
101: "Alice",
102: "Bob"
]
users[101] // "Alice"Here, Int is the key type and String is the value type.
Every key must be unique. If a value is assigned to an existing key, the old value is replaced:
users[101] = "Anna"Looking up a key returns an optional because the key might not exist:
users[999] // nilDictionary keys must conform to Hashable. Swift uses the key's hash to find its value efficiently.
2. Why do dictionary keys need to be Hashable?
A dictionary needs to find a key without checking every entry one by one.
Consider this lookup:
let scores = [
"Alice": 10,
"Bob": 20
]
let score = scores["Alice"]Swift takes "Alice", calculates its hash, and uses that hash to determine where in the table it should look.
Once it finds a possible match, it uses == to confirm that it found the correct key.
This gives Dictionary its fast average O(1) lookup.
There is one important rule:
If a == b, they must produce the same hash.The reverse is not required. Two different values can have the same hash.
3. What happens when two keys have the same hash?
Two different keys can produce the same hash. This is called a hash collision.
Collisions are normal. A hash is not a unique identifier for a key.
When a collision happens, Dictionary handles it internally and uses == to determine which key is the one being searched for.
This is why hashing and equality have different jobs:
- The hash helps Swift find where to look.
==confirms that it found the correct key.
Two keys can therefore have the same hash and still exist as separate entries in the dictionary.
4. Is Dictionary lookup really O(1)?
Lookup by key is O(1) on average.
let user = users[id]Swift can use the key's hash to find where the value should be instead of checking every entry.
However, not every operation on a dictionary is O(1).
For example:
users.first { $0.key == id } // ❌ O(n)This checks entries one by one. If the goal is to look up a key, use the dictionary's keyed lookup:
users[id] // ✅ Average O(1)Modern Swift also specializes membership checks on the dictionary's keys view:
users.keys.contains(id) // Average O(1)Searching for a value is O(n):
users.values.contains(user) // O(n)A dictionary is optimized for finding keys, not values.
O(1) is the average case rather than a guarantee. In a pathological case with many collisions, lookup can become slower.
5. What happens if a key changes after being inserted?
A key becomes problematic when a property used by its hash or equality changes after insertion.
The example below is intentionally wrong:
final class User: Hashable {
var id: Int
init(id: Int) {
self.id = id
}
static func == (lhs: User, rhs: User) -> Bool {
lhs.id == rhs.id
}
func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
}
let user = User(id: 1)
var users = [user: "Alice"]
user.id = 99 // ❌ Hash-relevant state changedWhen user was inserted, its ID was 1. The dictionary used that value to decide where the entry belonged.
After changing the ID to 99, the object produces a different hash. The dictionary can now look in a different place when searching for it and fail to find the original entry.
The object has not disappeared from memory. The problem is that its identity no longer matches the identity that was used when it was inserted.
For this reason, properties involved in Hashable and == should remain unchanged while the value is being used as a dictionary key.
6. What happens when you assign nil to a dictionary value?
Assigning nil through a dictionary subscript removes the entry.
var scores = [
"Alice": 10,
"Bob": 20
]
scores["Alice"] = nilAfter this, "Alice" is no longer in the dictionary.
The same operation can be written explicitly:
scores.removeValue(forKey: "Alice")There is a subtle difference if the dictionary stores optional values:
var scores: [String: Int?] = [
"Alice": 10
]
scores["Alice"] = .some(nil)In this case, "Alice" remains in the dictionary and its stored value is nil.
7. Can you modify a Dictionary while iterating over it?
Structural changes such as adding or removing entries should not be performed while iterating over the dictionary's current keys.
For example:
for key in users.keys {
users.removeValue(forKey: key) // ❌ Mutation during iteration
}If entries need to be removed while looping, first make a separate copy of the keys:
for key in Array(users.keys) {
users.removeValue(forKey: key) // ✅ Iterating over a snapshot
}Now the loop is iterating over an Array, so changing the dictionary does not change the collection being iterated.
If every entry needs to be removed, the simpler solution is:
users.removeAll()8. Does Dictionary preserve insertion order?
A dictionary should not be used when the correctness of the program depends on iteration order.
Its main purpose is fast lookup by key, not maintaining an ordered sequence.
If order matters, represent it explicitly.
For example:
var usersByID: [Int: User] = [:]
var orderedIDs: [Int] = []The dictionary provides fast lookup:
usersByID[id]The array represents the required order.
This keeps the two responsibilities separate: the dictionary answers which value belongs to this key, while the array answers in what order should these values appear.
9. How does Copy-on-Write work with Dictionary?
Dictionary is a value type.
That means assigning one dictionary to another gives you two independent values:
var first = ["A": 1]
var second = first
second["B"] = 2
print(first) // ["A": 1]
print(second) // ["A": 1, "B": 2]Although they behave as independent values, Swift does not need to copy all of the underlying storage immediately when this happens:
var second = firstThe two dictionaries can initially share the same storage.
A real copy becomes necessary when one of them is modified:
second["B"] = 2Swift sees that the storage is shared, creates a separate copy for second, and then performs the mutation.
This is Copy-on-Write (CoW).
It allows Dictionary to keep value semantics without paying the cost of copying its entire storage every time a dictionary is assigned or passed around.
10. What does reserveCapacity do?
A dictionary sometimes needs to allocate more storage as it grows.
If the expected number of entries is already known, reserveCapacity can prepare enough capacity in advance:
var users: [Int: User] = [:]
users.reserveCapacity(10_000)Without this, the dictionary may need to grow several times while those 10,000 users are inserted. Growing the table can involve allocating more storage and reorganizing existing entries.
Reserving capacity can reduce that work.
Removing entries also does not necessarily mean all of the allocated storage is immediately discarded.
For example:
users.removeAll(keepingCapacity: true)This removes the entries but keeps the allocated capacity so it can be reused later.
This can be useful when a large dictionary is repeatedly emptied and filled again.
11. How should Hashable and == work together?
== defines when two values represent the same key. hash(into:) must be consistent with that definition.
Consider:
struct User: Hashable {
let id: Int
let name: String
static func == (lhs: User, rhs: User) -> Bool {
lhs.id == rhs.id && lhs.name == rhs.name
}
func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
}Equality uses both id and name, while the hash only uses id.
This does not violate the fundamental Hashable rule because equal users still hash consistently. However, users with the same ID but different names are more likely to collide unnecessarily.
If both properties define equality, both can participate in hashing:
func hash(into hasher: inout Hasher) {
hasher.combine(id)
hasher.combine(name)
}Another valid design might decide that only id represents a user's identity. In that case, both equality and hashing should be based on id.
The important rule is simple: if two values are equal, they must hash consistently.
12. Is Dictionary thread-safe?
A dictionary should not be mutated concurrently from multiple threads.
Concurrent reads are fine when nobody is modifying the dictionary. Problems appear when a read overlaps with a write, or when multiple writes happen at the same time.
That creates a data race.
Shared mutable dictionaries therefore need synchronization. One option is to keep the dictionary inside an actor:
actor UserStore {
private var users: [Int: User] = [:]
func user(for id: Int) -> User? {
users[id]
}
func add(_ user: User, id: Int) {
users[id] = user
}
}The actor controls access to its state, preventing unrelated tasks from modifying the dictionary at the same time.
Locks and serial queues can also be used when appropriate.
The important point is that Dictionary does not provide synchronization by itself.
Why it matters
Understanding the underlying model explains many dictionary bugs and performance problems.
- Changing hash-relevant state can make an existing key impossible to find.
- Linear searches such as
first { $0.key == … }can turn an expected O(1) lookup into O(n). - Concurrent mutation can cause data races.
- Copy-on-Write explains why dictionaries behave like values without always being expensive to copy.
- Capacity management matters when large dictionaries are repeatedly created or rebuilt.
These behaviors all follow from the same underlying idea: Dictionary is a value type built on top of a hash table.
Interview angle
Start with the basic model: Dictionary is a value type backed by a hash table, with average O(1) lookup, insertion, and removal.
Then explain how Hashable helps locate a key and how == confirms it, including what happens when collisions occur. From there, cover the practical consequences: hash-relevant state must remain stable, not every dictionary operation is O(1), and shared mutation requires synchronization.
For a deeper discussion, explain Copy-on-Write and the basic pieces needed to build a hash table: buckets, collision handling, resizing, deletion, and equality.