Python Dictionaries : The Complete Guide to Key-Value Mastery

A dictionary is one of Python’s most powerful and most-used built-in data structures. It stores data as key-value pairs — each unique key maps to a value, just like a real-world dictionary maps a word to its definition. Dictionaries are the backbone of Python itself: function keyword arguments, object attributes, JSON data, environment configurations, and much more are all represented as dictionaries under the hood.

Introduced as a hash map, Python dictionaries offer O(1) average-time complexity for lookups, insertions, and deletions. Since Python 3.7, they also preserve insertion order, making them even more versatile.

This blog covers everything from creating your first dictionary to advanced patterns like nested dicts, defaultdict, Counter, and dictionary comprehensions — with output tables after every example so you can see exactly what each operation produces.

Dictionaries can be created in four different ways depending on context and preference.

# Curly-brace literal — most common way
student = {
'name': 'Priya Sharma',
'age': 22,
'grade': 'A',
'score': 91.5
}
print(student)

 

OUTPUT: print(student)

Output
{‘name’: ‘Priya Sharma’, ‘age’: 22, ‘grade’: ‘A’, ‘score’: 91.5}

# Using the dict() built-in with keyword arguments
product = dict(name='Laptop', brand='Dell', price=75000, in_stock=True)
print(product)
# From a list of (key, value) tuples
pairs = [('city', 'Delhi'), ('country', 'India'), ('pin', 110001)]
location = dict(pairs)
print(location)

 

OUTPUT: dict() from keyword args and from tuple pairs

VariableOutput
product{‘name’: ‘Laptop’, ‘brand’: ‘Dell’, ‘price’: 75000, ‘in_stock’: True}
location{‘city’: ‘Delhi’, ‘country’: ‘India’, ‘pin’: 110001}

# Create a dict with predefined keys, all sharing the same default value
subjects = ['Math', 'Science', 'English', 'History']
scores = dict.fromkeys(subjects, 0) # all start at 0
print(scores)
# Default value None when omitted
profile = dict.fromkeys(['name', 'email', 'phone'])
print(profile)

 

OUTPUT: fromkeys() with default values

VariableOutput
scores{‘Math’: 0, ‘Science’: 0, ‘English’: 0, ‘History’: 0}
profile{‘name’: None, ’email’: None, ‘phone’: None}

empty1 = {} # literal empty dict
empty2 = dict() # constructor empty dict
print(type(empty1), len(empty1)) # <class 'dict'> 0

 

OUTPUT: Empty dictionary type and length

Output
<class ‘dict’> 0

Dictionary values are accessed by their key. Python offers two ways: direct bracket notation and the safer .get() method.

employee = {'id': 101, 'name': 'Arjun', 'dept': 'Engineering', 'salary': 85000}
print(employee['name']) # direct access
print(employee['salary'])
# KeyError if key doesn't exist
# print(employee['age']) # KeyError: 'age'

 

OUTPUT: Accessing name and salary

ExpressionOutput
employee[‘name’]Arjun
employee[‘salary’]85000

employee = {'id': 101, 'name': 'Arjun', 'dept': 'Engineering', 'salary': 85000}
print(employee.get('dept')) # returns value if key exists
print(employee.get('age')) # returns None (key missing, no error)
print(employee.get('age', 'N/A')) # returns 'N/A' as default
print(employee.get('salary', 0)) # key exists, so returns 85000

 

OUTPUT: .get() — safe key access

ExpressionOutput
employee.get(‘dept’)Engineering
employee.get(‘age’)None
employee.get(‘age’, ‘N/A’)N/A
employee.get(‘salary’, 0)85000

Best practice: always use .get() when you are not certain a key exists. It avoids KeyError and lets you specify a sensible default.

Dictionaries are mutable — you can add new keys, change existing values, and remove entries at any time.

inventory = {'apples': 50, 'bananas': 30}
print('Before:', inventory)
inventory['oranges'] = 45 # add a new key
inventory['grapes'] = 20
print('After:', inventory)

 

OUTPUT: Adding new keys to a dictionary

StateDictionary
Before{‘apples’: 50, ‘bananas’: 30}
After{‘apples’: 50, ‘bananas’: 30, ‘oranges’: 45, ‘grapes’: 20}

prices = {'rice': 60, 'wheat': 45, 'sugar': 55}
print('Before:', prices)
prices['rice'] = 65 # update existing key
prices['sugar'] = 58
print('After:', prices)

 

OUTPUT: Updating existing values

KeyBeforeAfter
rice6065
wheat4545  (unchanged)
sugar5558

config = {'host': 'localhost', 'port': 5432, 'db': 'mydb', 'debug': True}
# del — remove key (KeyError if missing)
del config['debug']
print('After del:', config)
# pop() — remove and return the value
port = config.pop('port')
print('Popped port:', port)
print('After pop:', config)
# pop() with default (no error if key missing)
val = config.pop('timeout', 30)
print('Default pop:', val)
# popitem() — remove and return last inserted (key, value) pair
item = config.popitem()
print('Popped item:', item)
# clear() — remove ALL entries
config.clear()
print('After clear:', config)

 

OUTPUT: Step-by-step deletion operations

OperationReturn ValueDictionary After
del config[‘debug’]{‘host’: ‘localhost’, ‘port’: 5432, ‘db’: ‘mydb’}
config.pop(‘port’)5432{‘host’: ‘localhost’, ‘db’: ‘mydb’}
config.pop(‘timeout’, 30)30{‘host’: ‘localhost’, ‘db’: ‘mydb’}  (unchanged)
config.popitem()(‘db’,’mydb’){‘host’: ‘localhost’}
config.clear(){}

MethodDescriptionReturns
d.keys()All keysdict_keys view
d.values()All valuesdict_values view
d.items()All key-value pairs as tuplesdict_items view
d.get(k, def)Value for key k, or defaultvalue or default
d.update(other)Merge another dict / iterable into dNone (in-place)
d.setdefault(k,v)Return value if key exists, else set itvalue
d.pop(k, def)Remove key and return valuevalue or default
d.popitem()Remove and return last (k, v) pair(key, value)
d.copy()Shallow copy of the dictionarynew dict
d.clear()Remove all itemsNone
d.fromkeys(k, v)New dict from keys with optional valuenew dict
len(d)Number of key-value pairsint
k in dTrue if key k existsbool
k not in dTrue if key k does not existbool

person = {'name': 'Rohan', 'city': 'Mumbai', 'age': 28, 'job': 'Developer'}
print(person.keys()) # dict_keys(['name', 'city', 'age', 'job'])
print(person.values()) # dict_values(['Rohan', 'Mumbai', 28, 'Developer'])
print(person.items()) # dict_items([('name','Rohan'),('city','Mumbai'),...])
# Convert to list if you need indexing
keys_list = list(person.keys())
print(keys_list) # ['name', 'city', 'age', 'job']

 

OUTPUT: keys(), values(), and items()

MethodOutput
person.keys()dict_keys([‘name’, ‘city’, ‘age’, ‘job’])
person.values()dict_values([‘Rohan’, ‘Mumbai’, 28, ‘Developer’])
person.items()dict_items([(‘name’,’Rohan’), (‘city’,’Mumbai’), (‘age’,28), (‘job’,’Developer’)])
list(person.keys())[‘name’, ‘city’, ‘age’, ‘job’]

base = {'name': 'Sita', 'age': 25, 'city': 'Pune'}
extra = {'age': 26, 'email': 'sita@example.com', 'phone': '9876543210'}
base.update(extra) # merges extra into base; overlapping keys are overwritten
print(base)
# Python 3.9+ merge operator |
merged = base | {'tier': 'Gold'}
print(merged)

 

OUTPUT: update() merges and overwrites — age changed to 26

VariableOutput
base (after update){‘name’: ‘Sita’, ‘age’: 26, ‘city’: ‘Pune’, ’email’: ‘sita@example.com’, ‘phone’: ‘9876543210’}
merged{‘name’: ‘Sita’, ‘age’: 26, ‘city’: ‘Pune’, ’email’: ‘sita@example.com’, ‘phone’: ‘9876543210’, ‘tier’: ‘Gold’}

user = {'name': 'Amit', 'email': 'amit@mail.com'}
# Key exists — returns existing value, does NOT overwrite
v1 = user.setdefault('name', 'Unknown')
print(v1) # Amit (existing value returned)
# Key missing — inserts with default and returns default
v2 = user.setdefault('role', 'viewer')
print(v2) # viewer
print(user) # role has been added

 

OUTPUT: setdefault() — only inserts if key is absent

ExpressionOutput
v1 = user.setdefault(‘name’,’Unknown’)Amit  (key existed, no change)
v2 = user.setdefault(‘role’,’viewer’)viewer  (key added)
print(user){‘name’: ‘Amit’, ’email’: ‘amit@mail.com’, ‘role’: ‘viewer’}

You can iterate over a dictionary in three ways: over keys (default), over values, or over key-value pairs simultaneously.

marks = {'Maths': 88, 'Science': 92, 'English': 79, 'History': 85}
for subject in marks: # iterates keys by default
print(subject)

 

OUTPUT: Iterating keys

Output (one per line)
Maths
Science
English
History

for score in marks.values():
print(score)

 

OUTPUT: Iterating values

Output (one per line)
88
92
79
85

for subject, score in marks.items():
print(f'{subject}: {score}')

 

OUTPUT: Iterating key-value pairs

Output
Maths: 88
Science: 92
English: 79
History: 85

for idx, (subject, score) in enumerate(marks.items(), start=1):
print(f'{idx}. {subject} -> {score}')

 

OUTPUT: Enumerated key-value pairs

Output
1. Maths -> 88
2. Science -> 92
3. English -> 79
4. History -> 85

Dictionary comprehensions provide a concise, readable way to create or transform dictionaries in a single expression. They follow the pattern: {key_expr: value_expr for item in iterable if condition}.

squares = {x: x**2 for x in range(1, 7)}
print(squares)

 

OUTPUT: Squares dictionary

Output
{ 1:1, 2:4, 3:9, 4:16, 5:25, 6:36 }

all_scores = {'Alice': 72, 'Bob': 88, 'Carol': 55, 'David': 93, 'Eve': 61}
# Keep only students who passed (score >= 70)
passed = {name: score for name, score in all_scores.items() if score >= 70}
print(passed)
# Grade mapping
grades = {name: 'Pass' if s >= 70 else 'Fail' for name, s in all_scores.items()}
print(grades)

 

OUTPUT: Filtered and transformed comprehensions

VariableOutput
passed{‘Alice’: 72, ‘Bob’: 88, ‘David’: 93}
grades{‘Alice’: ‘Pass’, ‘Bob’: ‘Pass’, ‘Carol’: ‘Fail’, ‘David’: ‘Pass’, ‘Eve’: ‘Fail’}

country_code = {'India': 'IN', 'Germany': 'DE', 'Japan': 'JP', 'Brazil': 'BR'}
# Invert: code -> country
code_country = {code: country for country, code in country_code.items()}
print(code_country)

 

OUTPUT: Inverted dictionary

Output
{‘IN’: ‘India’, ‘DE’: ‘Germany’, ‘JP’: ‘Japan’, ‘BR’: ‘Brazil’}

subjects = ['Maths', 'Physics', 'Chemistry', 'Biology']
scores = [88, 76, 91, 83]
result = {sub: sc for sub, sc in zip(subjects, scores)}
print(result)

 

OUTPUT: Dictionary built from two lists with zip

Output
{‘Maths’: 88, ‘Physics’: 76, ‘Chemistry’: 91, ‘Biology’: 83}

A dictionary’s values can themselves be dictionaries. This is ideal for representing hierarchical data like student records, JSON-like structures, or organisational trees.

school = {
'Class_A': {
'Priya': {'math': 90, 'science': 85},
'Rohan': {'math': 78, 'science': 92},
},
'Class_B': {
'Sara': {'math': 88, 'science': 79},
'Amit': {'math': 95, 'science': 91},
}
}
# Access nested value
print(school['Class_A']['Priya']['math']) # 90
# Safe nested access with get()
score = school.get('Class_A', {}).get('Rohan', {}).get('science', 0)
print(score) # 92

 

OUTPUT: Nested dictionary access

ExpressionOutput
school[‘Class_A’][‘Priya’][‘math’]90
school.get(‘Class_A’,{}).get(‘Rohan’,{}).get(‘science’,0)92

for class_name, students in school.items():
print(f'--- {class_name} ---')
for student, marks in students.items():
avg = sum(marks.values()) / len(marks)
print(f' {student}: avg = {avg:.1f}')

 

OUTPUT: Nested iteration output

Output
— Class_A —
  Priya: avg = 87.5
  Rohan: avg = 85.0
— Class_B —
  Sara:  avg = 83.5
  Amit:  avg = 93.0

Python’s collections module extends the built-in dict with three specialised variants, each solving a common pattern more elegantly.

from collections import defaultdict
# defaultdict(list) — missing key automatically gets an empty list
groups = defaultdict(list)
students = [('A', 'Priya'), ('B', 'Rohan'), ('A', 'Sara'), ('B', 'Amit'), ('A', 'Kiran')]
for cls, name in students:
groups[cls].append(name) # no KeyError even on first access
print(dict(groups))
# defaultdict(int) for counting
word_count = defaultdict(int)
sentence = 'the cat sat on the mat the cat'.split()
for word in sentence:
word_count[word] += 1
print(dict(word_count))

 

OUTPUT: defaultdict — auto-initialised keys

VariableOutput
groups{‘A’: [‘Priya’, ‘Sara’, ‘Kiran’], ‘B’: [‘Rohan’, ‘Amit’]}
word_count{‘the’: 3, ‘cat’: 2, ‘sat’: 1, ‘on’: 1, ‘mat’: 1}

from collections import Counter
# Count characters in a string
text = 'mississippi'
char_count = Counter(text)
print(char_count)
# Most common elements
print(char_count.most_common(3))
# Count words in a sentence
words = 'apple banana apple cherry banana apple'.split()
fruit_count = Counter(words)
print(fruit_count)
# Arithmetic on Counters
c1 = Counter({'a': 3, 'b': 1})
c2 = Counter({'a': 1, 'b': 2, 'c': 5})
print(c1 + c2) # add counts

 

OUTPUT: Counter operations

ExpressionOutput
Counter(‘mississippi’)Counter({‘s’: 4, ‘i’: 4, ‘p’: 2, ‘m’: 1})
char_count.most_common(3)[(‘s’, 4), (‘i’, 4), (‘p’, 2)]
Counter(words)Counter({‘apple’: 3, ‘banana’: 2, ‘cherry’: 1})
c1 + c2Counter({‘a’: 4, ‘c’: 5, ‘b’: 3})

from collections import OrderedDict
# Useful before Python 3.7 (regular dicts now preserve order too)
od = OrderedDict()
od['first'] = 1
od['second'] = 2
od['third'] = 3
print(od)
# Move a key to the end or beginning
od.move_to_end('first')
print(od) # 'first' is now last
od.move_to_end('third', last=False)
print(od) # 'third' is now first

 

OUTPUT: OrderedDict ordering operations

StateOutput
InitialOrderedDict([(‘first’,1), (‘second’,2), (‘third’,3)])
After move_to_end(‘first’)OrderedDict([(‘second’,2), (‘third’,3), (‘first’,1)])
After move_to_end(‘third’, last=False)OrderedDict([(‘third’,3), (‘second’,2), (‘first’,1)])

config = {'debug': True, 'host': 'localhost', 'port': 8080, 'timeout': 30}
# in — check if key exists
print('host' in config) # True
print('password' in config) # False
# not in
print('api_key' not in config) # True
# Check value existence (use .values())
print(8080 in config.values()) # True
# Conditional access pattern
if 'debug' in config and config['debug']:
print('Debug mode is ON')

 

OUTPUT: Membership and existence checks

ExpressionOutput
‘host’ in configTrue
‘password’ in configFalse
‘api_key’ not in configTrue
8080 in config.values()True
Debug mode is ON(printed by if block)

Dictionaries themselves are not sorted by value, but you can produce sorted views or convert to a sorted structure easily.

prices = {'banana': 20, 'apple': 45, 'cherry': 80, 'date': 35}
sorted_by_key = dict(sorted(prices.items()))
print(sorted_by_key)

 

OUTPUT: Sorted by key (alphabetical)

Output
{‘apple’: 45, ‘banana’: 20, ‘cherry’: 80, ‘date’: 35}

# Ascending by value
sorted_by_val_asc = dict(sorted(prices.items(), key=lambda item: item[1]))
print('Ascending:', sorted_by_val_asc)
# Descending by value
sorted_by_val_desc = dict(sorted(prices.items(), key=lambda item: item[1], reverse=True))
print('Descending:', sorted_by_val_desc)

 

OUTPUT: Sorted by value ascending and descending

OrderOutput
Ascending{‘banana’: 20, ‘date’: 35, ‘apple’: 45, ‘cherry’: 80}
Descending{‘cherry’: 80, ‘apple’: 45, ‘date’: 35, ‘banana’: 20}

Python 3 offers several ways to merge dictionaries, each with slightly different semantics.

d1 = {'a': 1, 'b': 2, 'c': 3}
d2 = {'c': 30, 'd': 4, 'e': 5} # 'c' exists in both — d2 wins
# Method 1: update() (in-place, modifies d1)
merged1 = d1.copy()
merged1.update(d2)
print('update():', merged1)
# Method 2: ** unpacking (creates new dict)
merged2 = {**d1, **d2}
print('** unpack:', merged2)
# Method 3: | operator (Python 3.9+, creates new dict)
merged3 = d1 | d2
print('| operator:', merged3)
# Method 4: |= operator (Python 3.9+, in-place)
d3 = {'x': 10, 'y': 20}
d3 |= {'y': 99, 'z': 30}
print('|= operator:', d3)

 

OUTPUT: All four merge methods

MethodOutput
update(){‘a’: 1, ‘b’: 2, ‘c’: 30, ‘d’: 4, ‘e’: 5}
** unpack{‘a’: 1, ‘b’: 2, ‘c’: 30, ‘d’: 4, ‘e’: 5}
| operator{‘a’: 1, ‘b’: 2, ‘c’: 30, ‘d’: 4, ‘e’: 5}
|= operator{‘x’: 10, ‘y’: 99, ‘z’: 30}

import copy
original = {'name': 'Anjali', 'scores': [90, 85, 78]}
# Shallow copy — top-level keys are independent, nested objects are shared
shallow = original.copy()
shallow['name'] = 'Priya' # does NOT affect original
shallow['scores'].append(95) # DOES affect original (shared list!)
print('original:', original)
print('shallow: ', shallow)
# Deep copy — fully independent at all levels
deep = copy.deepcopy(original)
deep['scores'].append(100) # does NOT affect original
print('After deep change:')
print('original:', original)
print('deep: ', deep)

 

OUTPUT: Shallow vs deep copy behaviour

After shallow[‘scores’].append(95)Value
original[‘scores’][90, 85, 78, 95]  (shared — affected!)
shallow[‘scores’][90, 85, 78, 95]  (same list object)
original[‘name’]Anjali  (string is independent — not affected)
shallow[‘name’]Priya

Rule: use dict.copy() or {**d} for flat (no nested) dicts. Use copy.deepcopy() whenever values contain mutable objects like lists, sets, or other dicts.

  1. Use .get(key, default) instead of direct bracket access when a key may be absent.
  2. Use dict comprehensions instead of for-loop-with-setitem for clean, readable transformations.
  3. Prefer defaultdict(list/int/set) when building group-by or counting structures.
  4. Use Counter for any counting task — it is far more readable than a manual dict.
  5. Use copy.deepcopy() when cloning dicts that contain mutable nested values.
  6. Use the | merge operator (Python 3.9+) for clean, non-mutating dict merges.
  7. Keep dictionary keys consistent types (all strings, all ints) for predictability.
  8. For large, frequently-read dicts, consider __slots__ or dataclasses as a faster alternative.
  9. Use meaningful key names — avoid abbreviations that reduce readability.
  10. Use dict.items() in for-loops instead of dict[key] lookups for cleaner, faster iteration.

OperationSyntax / ExampleNotes
Created = {‘k’: v}  or  dict(k=v)Literal or constructor
Accessd[‘key’]  or  d.get(‘key’, default).get() is safer
Add / Updated[‘key’] = valueCreates or overwrites
Delete keydel d[‘key’]  or  d.pop(‘key’)del raises KeyError if missing
All keysd.keys()Returns view object
All valuesd.values()Returns view object
All pairsd.items()Returns (k,v) tuples
Merge (in-place)d.update(other)Overwrites duplicates
Merge (new dict)d1 | d2  or  {**d1, **d2}3.9+ for |
Check key‘key’ in dO(1) lookup
Set if missingd.setdefault(‘key’, default)Inserts only if absent
Remove last paird.popitem()Returns (k, v) tuple
Remove alld.clear()Empties the dict
Shallow copyd.copy()  or  {**d}Nested objects shared
Deep copycopy.deepcopy(d)Fully independent
Comprehension{k: v for k, v in items if cond}Concise transformation
Count occurrencesCounter(iterable)collections.Counter
Auto-defaultdefaultdict(list)collections.defaultdict
Sort by keydict(sorted(d.items()))Alphabetical by default
Sort by valuedict(sorted(d.items(), key=lambda x: x[1]))lambda on value

Python dictionaries are far more than a simple key-value store. With O(1) lookups, preserved insertion order, a rich set of methods, and powerful variants like defaultdict and Counter, they are the go-to data structure for a vast range of programming tasks — from counting and grouping to configuration management and JSON parsing.

The foundation is simple: create with {}, access with .get(), iterate with .items(), and transform with comprehensions. Layer on defaultdict for automatic defaults, Counter for frequency analysis, and copy.deepcopy() for safe cloning — and you have a complete dictionary toolkit for any Python project.

Happy Coding!


Discover more from DataSangyan

Subscribe to get the latest posts sent to your email.

Leave a Reply