Dictionary (Key-Value Pairs)
Reviewed & published by Brayan K
A List answers "what's at position 5?". A Dictionary answers "what's the value for this key?" — a username, a product code, a country. It maps unique keys to values and finds them almost instantly, which makes it the go-to structure for lookups, caches, and counting. It's one of the most useful collections you'll ever learn.
Part of the free C# course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
What You'll Learn
- Create a Dictionary<TKey, TValue> and add entries
- Look up, overwrite, and remove by key
- Avoid crashes with ContainsKey and TryGetValue
- Iterate with KeyValuePair and foreach
- GetValueOrDefault for clean fallbacks
- The counting / tally pattern everyone uses
💡 Real-World Analogy
A dictionary is exactly like a physical dictionary or phone book. You don't read every page to find "aardvark" — you jump straight to the word (the key) and read its definition (the value). Each word appears once, and lookup is instant because the book is organised for it. That's why C# borrowed the name: unique keys, fast lookups, one definition per word.
1. Creating & Using a Dictionary
Declare a dictionary with both types: Dictionary<string, string> maps text keys to text values. Add entries with Add or the indexer, read with dict[key], and check the size with Count. The indexer overwrites an existing key; Add throws if the key is already present. Iterate the pairs with foreach over KeyValuePair.
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
// Dictionary<TKey, TValue> maps a unique KEY to a VALUE.
// Here: a country name (key) -> its capital city (value).
Dictionary<string, string> capitals = new Dictionary<string, string>
{
["France"] = "Paris",
["Japan"] = "Tokyo",
["Brazil"] = "Brasília"
};
// Add another entry. Add throws if the key already exists.
capitals.Add("Egypt", "Cairo");
// Look up by key — near-instant, no scanning.
Console.WriteLine($"Capital of Japan: {capitals["Japan"]}"); // Tokyo
Console.WriteLine($"Entries: {capitals.Count}"); // Entries: 4
// The indexer OVERWRITES if the key already exists.
capitals["France"] = "Paris (City of Light)";
Console.WriteLine(capitals["France"]);
// Iterate every key/value pair.
foreach (KeyValuePair<string, string> pair in capitals)
Console.WriteLine($" {pair.Key} -> {pair.Value}");
}
}
// ✅ Expected output:
// Capital of Japan: Tokyo
// Entries: 4
// Paris (City of Light)
// France -> Paris (City of Light)
// Japan -> Tokyo
// Brazil -> Brasília
// Egypt -> Cairo2. Reading Safely
The biggest beginner trap: reading a key that isn't there throws KeyNotFoundException. The two safe approaches are ContainsKey (check, then read) and TryGetValue (check and fetch in one lookup). Prefer TryGetValue — it's faster and reads cleanly.
using System.Globalization;
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
// Currency formatting follows the thread's culture: {value:C} prints
// £ in London, $ in Boston and € in Paris. Pin it when the output has
// to be the same everywhere — as it does on a page that shows you the
// result.
CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo("en-GB");
var prices = new Dictionary<string, decimal>
{
["coffee"] = 2.50m,
["tea"] = 1.80m
};
// ❌ Reading a MISSING key with the indexer throws:
// var x = prices["juice"]; // 💥 KeyNotFoundException
// ✅ ContainsKey checks first.
if (prices.ContainsKey("tea"))
Console.WriteLine($"Tea costs {prices["tea"]:C}"); // Tea costs £1.80
// ✅ TryGetValue checks AND fetches in one lookup — preferred.
if (prices.TryGetValue("juice", out decimal price))
Console.WriteLine($"Juice: {price:C}");
else
Console.WriteLine("No price for juice"); // this runs
// Remove returns true if the key was there.
bool removed = prices.Remove("coffee");
Console.WriteLine($"Removed coffee? {removed}, now {prices.Count} left");
// Removed coffee? True, now 1 left
}
}
// ✅ Expected output:
// Tea costs £1.80
// No price for juice
// Removed coffee? True, now 1 leftYour turn. Build a small phone book — add a contact and look one up safely.
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
// 🎯 YOUR TURN — a tiny phone book.
var phoneBook = new Dictionary<string, string>
{
["Alice"] = "0123 456",
["Bob"] = "0987 654"
};
// 1) Add a new contact: "Carol" -> "0555 111" (hint: indexer or Add)
___; // 👉 phoneBook["Carol"] = "0555 111";
// 2) Safely look up "Dave" with TryGetValue; print his number or
// "Not found" if he isn't there.
if (phoneBook.TryGetValue("Dave", out string number))
Console.WriteLine($"Dave: {number}");
else
Console.WriteLine("Not found"); // 👈 this should run
Console.WriteLine($"Contacts: {phoneBook.Count}");
// ✅ Expected: Not found / Contacts: 3
}
}3. The Counting Pattern
Counting occurrences is the single most common dictionary task — tallying votes, word frequencies, items in a cart. The idea: for each item, if the key exists add one, otherwise start it at one. This pattern shows up constantly, so it's worth committing to memory.
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
// The CLASSIC dictionary use: counting occurrences.
string sentence = "the cat sat on the mat the cat ran";
string[] words = sentence.Split(' ');
var counts = new Dictionary<string, int>();
foreach (string word in words)
{
// If we've seen it, add 1; otherwise start at 0 then add 1.
if (counts.ContainsKey(word))
counts[word]++;
else
counts[word] = 1;
}
foreach (var pair in counts)
Console.WriteLine($"{pair.Key}: {pair.Value}");
// the: 3
// cat: 2
// sat: 1
// on: 1
// mat: 1
// ran: 1
}
}
// ✅ Expected output:
// the: 3
// cat: 2
// sat: 1
// on: 1
// mat: 1
// ran: 1🔎 Deep Dive: GetValueOrDefault
GetValueOrDefault(key) returns the value if the key exists, or a fallback (the type's default, or one you supply) if it doesn't — never throwing. It collapses the whole counting pattern into a single elegant line: tally[c] = tally.GetValueOrDefault(c) + 1;.
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
var inventory = new Dictionary<string, int>
{
["apples"] = 12,
["bananas"] = 5
};
// GetValueOrDefault returns a fallback for missing keys (no throw).
int apples = inventory.GetValueOrDefault("apples"); // 12
int pears = inventory.GetValueOrDefault("pears", 0); // 0 (missing)
Console.WriteLine($"apples: {apples}, pears: {pears}");
// This makes the counting pattern a one-liner:
var tally = new Dictionary<char, int>();
foreach (char c in "banana")
tally[c] = tally.GetValueOrDefault(c) + 1;
foreach (var kv in tally)
Console.WriteLine($"'{kv.Key}' x {kv.Value}");
// 'b' x 1
// 'a' x 3
// 'n' x 2
}
}
// ✅ Expected output:
// apples: 12, pears: 0
// 'b' x 1
// 'a' x 3
// 'n' x 2A note on ordering: a standard Dictionary makes no promise about iteration order. If you need keys sorted, use a SortedDictionary, or sort the keys yourself when you enumerate.
🧩 Reorder Challenge
These six lines count letters in "moon" and print the count for 'o'. Order them so it prints o appears 2 times.
Console.WriteLine($"o appears {tally['o']} times");
var tally = new Dictionary<char, int>();
foreach (char c in word)
tally[c] = tally.GetValueOrDefault(c) + 1;
string word = "moon";
// (these five lines go inside Main)string word = "moon";
var tally = new Dictionary<char, int>();
foreach (char c in word)
tally[c] = tally.GetValueOrDefault(c) + 1;
Console.WriteLine($"o appears {tally['o']} times");Why: word must exist before the loop reads it, and the tally dictionary must exist before the loop fills it. The loop must finish counting before the final WriteLine reads tally['o'], which by then is 2.
🧠 Quick Recall
1. What does this print?
var d = new Dictionary<string, int>();
d["a"] = 1;
d["a"] = 5;
Console.WriteLine(d["a"]);5 — assigning with the indexer to an existing key overwrites it, so the second assignment wins.
2. What is the output?
var d = new Dictionary<string, int> { ["x"] = 9 };
bool ok = d.TryGetValue("y", out int v);
Console.WriteLine($"{ok} {v}");False 0 — key "y" is absent, so TryGetValue returns false and sets v to its default, 0.
3. What prints here?
var d = new Dictionary<string, int> { ["n"] = 3 };
Console.WriteLine(d.GetValueOrDefault("m", -1));-1 — key "m" doesn't exist, so GetValueOrDefault returns the supplied fallback of -1.
Pro Tips
- 💡 TryGetValue over ContainsKey + indexer — it does the check and fetch in one lookup.
- 💡 Indexer to overwrite, Add to insist on a new key. Pick based on whether a duplicate is a bug.
- 💡 GetValueOrDefault makes counting a one-liner and avoids the if/else.
- 💡 Don't rely on order. A Dictionary isn't ordered; use SortedDictionary if you need sorted keys.
Common Errors (and the fix)
- "KeyNotFoundException": you read a key that isn't there with the indexer. Use TryGetValue or check ContainsKey.
- "ArgumentException: an item with the same key has already been added": Add hit a duplicate. Use the indexer to overwrite instead.
- Relying on insertion order: a Dictionary may iterate in any order. Sort explicitly if order matters.
- "The given key was not present" when removing: harmless — Remove simply returns false for a missing key.
Mini-Challenge: Vote Tally
Count votes per colour with a dictionary, then find the winner. The outline is in the comments — fill in the body.
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
// 🎯 MINI-CHALLENGE: Vote tally
// 1. Given these votes:
// string[] votes = { "red", "blue", "red", "green", "blue", "red" };
// 2. Build a Dictionary<string, int> counting votes per colour
// (hint: counts[v] = counts.GetValueOrDefault(v) + 1;).
// 3. Print each colour and its count.
// 4. Find and print the WINNER (the colour with the most votes).
//
// ✅ Example output:
// red: 3
// blue: 2
// green: 1
// Winner: red with 3 votes
// your code here
}
}🎉 Lesson Complete
- ✅ Dictionary<TKey, TValue> maps unique keys to values for fast lookup
- ✅ Indexer overwrites; Add throws on a duplicate key
- ✅ Read safely with TryGetValue or ContainsKey — the indexer throws on a missing key
- ✅ Iterate pairs with foreach over KeyValuePair
- ✅ GetValueOrDefault powers the counting/tally pattern in one line
Practice quiz
What does a Dictionary<TKey, TValue> map?
- Positions to values
- Unique keys to values
- Values to keys only
- Indexes to a sorted list
Answer: Unique keys to values. A Dictionary maps a unique key to a value, and finds the value by key almost instantly.
What happens if you call Add with a key that already exists?
- It overwrites the value silently
- It returns false
- It throws ArgumentException
- It adds a duplicate entry
Answer: It throws ArgumentException. Add with a duplicate key throws ArgumentException; use the indexer dict[key] = value to overwrite instead.
What does the indexer dict[key] = value do when the key already exists?
- Throws an exception
- Overwrites the existing value silently
- Ignores the assignment
- Adds a second entry
Answer: Overwrites the existing value silently. Assigning with the indexer overwrites an existing key's value silently — handy when overwriting is intended.
What happens when you read a missing key with the indexer dict[key]?
- Returns null
- Returns the type's default
- Throws KeyNotFoundException
- Returns false
Answer: Throws KeyNotFoundException. Reading a key that isn't present with the indexer throws KeyNotFoundException — use TryGetValue or ContainsKey instead.
Why is TryGetValue preferred over ContainsKey + indexer?
- It sorts the dictionary
- It checks and fetches in a single lookup
- It never returns false
- It allows duplicate keys
Answer: It checks and fetches in a single lookup. TryGetValue does the existence check and the fetch in one lookup, so it's faster and cleaner than checking with ContainsKey then reading.
When a key is absent, what does TryGetValue do to the out parameter?
- Leaves it unassigned
- Sets it to the type's default and returns false
- Throws
- Sets it to null always
Answer: Sets it to the type's default and returns false. TryGetValue returns false and sets the out value to the type's default (e.g. 0 for int) — it never throws.
Does a standard Dictionary guarantee iteration order?
- Yes, insertion order
- Yes, sorted by key
- No, it makes no promise about order
- Only for string keys
Answer: No, it makes no promise about order. A standard Dictionary makes no promise about iteration order; use SortedDictionary for keys sorted by key.
What does GetValueOrDefault("m", -1) return when key "m" is absent?
- 0
- null
- -1
- Throws KeyNotFoundException
Answer: -1. GetValueOrDefault returns the supplied fallback (-1 here) for a missing key, never throwing.
Which one-liner implements the counting pattern with GetValueOrDefault?
- counts[c] = counts[c] + 1;
- counts[c] = counts.GetValueOrDefault(c) + 1;
- counts.Add(c, counts[c] + 1);
- counts[c]++ only;
Answer: counts[c] = counts.GetValueOrDefault(c) + 1;. counts[c] = counts.GetValueOrDefault(c) + 1; starts missing keys at 0 then increments — the whole tally pattern in one line.
What type do you get when you iterate a Dictionary with foreach?
- A KeyValuePair<TKey, TValue> per entry
- Just the keys
- Just the values
- A tuple of indexes
Answer: A KeyValuePair<TKey, TValue> per entry. Iterating a Dictionary yields a KeyValuePair<TKey, TValue> for each entry, exposing .Key and .Value.
Continue this course
- Previous: Generic Lists (List<T>)
- Next: Properties & Auto-Properties — get/set, auto-properties, init and computed properties
- Quick reference: C# cheat sheet › Collections
Frequently asked questions
When should I use a Dictionary instead of a List?
Use a Dictionary when you look things up by a unique key — a user ID to a user, a word to its count, a product code to a price. Looking up by key is roughly constant time, whereas finding an item in a List means scanning it. If you mostly add, iterate in order, or access by position, a List is the better fit.
What happens if I add a key that already exists?
Using the Add method with a duplicate key throws ArgumentException. If you instead assign with the indexer, dict[key] = value, it overwrites the existing value silently. So use Add when a duplicate would be a bug you want to catch, and the indexer when overwriting is intended.
How do I safely read a value that might not exist?
Reading a missing key with the indexer throws KeyNotFoundException. Use TryGetValue(key, out var value) which returns false if the key is absent and never throws, or ContainsKey to check first. TryGetValue is preferred because it checks and fetches in a single lookup.
Are dictionary items kept in the order I added them?
No. A standard Dictionary does not guarantee any particular iteration order — never rely on it. If you need to preserve insertion order while still keying by value, consider iterating a List of key/value pairs, or sort the keys explicitly when you enumerate. For ordered keys, SortedDictionary keeps them sorted by key.