This article offers 25 exercises to help you get comfortable with Python’s collections module through hands-on practice. You’ll explore Counter for frequency analysis and arithmetic operations; defaultdict for clean grouping and graph building; OrderedDict for order-aware data structures including an LRU cache implementation; deque for efficient queue, stack, and sliding-window patterns; and namedtuple for lightweight, readable record types
Each coding challenge includes a Practice Problem, Hint, Solution code, and detailed Explanation, ensuring you don’t just copy code, but genuinely practice and understand how and why it works.
- All solutions have been fully tested on Python 3.
- Use our Online Code Editor to solve these exercises in real time.
- Also, Solve Python Exercises: 29 topic-wise exercises with over 800+ coding questions
Let us know if you have any alternative solutions. It will help other developers.
+ Table of Contents (25 Exercises)
Table of contents
- Exercise 1: Word Frequency Counter
- Exercise 2: Most Common Elements
- Exercise 3: Subtract Two Counters
- Exercise 4: Character Frequency in a String
- Exercise 5: Combine Counters with Addition
- Exercise 6: Group Words by First Letter
- Exercise 7: Count Occurrences Without KeyError
- Exercise 8: Build an Adjacency List
- Exercise 9: Nested defaultdict
- Exercise 10: defaultdict with set
- Exercise 11: Preserve Insertion Order
- Exercise 12: Move to End
- Exercise 13: Implement a Simple LRU Cache
- Exercise 14: Compare Two OrderedDicts
- Exercise 15: Reverse an OrderedDict
- Exercise 16: Basic deque Operations
- Exercise 17: Implement a Queue (FIFO)
- Exercise 18: Implement a Stack (LIFO)
- Exercise 19: Rotating a deque
- Exercise 20: Bounded deque for Recent History
- Exercise 21: Create a namedtuple
- Exercise 22: namedtuple as a Lightweight Record
- Exercise 23: Convert namedtuple to Dictionary
- Exercise 24: Replace a Field Value
- Exercise 25: namedtuple with Default Values
Exercise 1: Word Frequency Counter
Problem Statement: Write a Python program that takes a sentence string and uses Counter to count how many times each word appears, then prints each word alongside its frequency.
Purpose: Counting word frequencies is one of the most common text processing tasks, appearing in search engines, NLP pipelines, content analysis tools, and spam filters. This exercise introduces Counter as a purpose-built, readable alternative to manually incrementing dictionary keys in a loop.
Given Input: sentence = "the cat sat on the mat the cat sat"
Expected Output: Each word and its count printed on a separate line, e.g. the: 3, cat: 2, sat: 2, on: 1, mat: 1
▼ Hint
- Import
Counterfrom thecollectionsmodule. - Split the sentence into a list of words using
sentence.split(), then pass the list directly toCounter(). - Iterate over the resulting
Counterobject with aforloop using.items()to access each word and its count. - Call
.lower()on the sentence before splitting to ensure the count is case-insensitive.
▼ Solution & Explanation
Explanation:
Counter(words): Accepts any iterable and returns a dictionary subclass where each unique element is a key and its count is the value. Passing the list of words directly produces the frequency map in a single call with no manual loop required.sentence.lower().split(): Lowercasing before splitting ensures that words like"The"and"the"are treated as the same token.str.split()with no arguments splits on any whitespace and discards empty strings automatically.word_count.items(): BecauseCounteris a subclass ofdict, it supports all standard dictionary methods including.items(),.keys(), and.values(), making iteration familiar and consistent.
Exercise 2: Most Common Elements
Problem Statement: Write a Python program that takes a list of items and uses Counter.most_common(n) to find and print the top 3 most frequently occurring elements along with their counts.
Purpose: Finding the top N most frequent elements is a classic problem in data analysis, log monitoring, and recommendation systems. The most_common() method solves it in one line without sorting manually, making this one of the most practically useful features of Counter.
Given Input: items = ["apple", "banana", "apple", "cherry", "banana", "apple", "date", "cherry", "banana", "apple"]
Expected Output: apple: 4, banana: 3, cherry: 2
▼ Hint
- Pass the list to
Counter()to get the frequency map, then call.most_common(3)on the result. .most_common(n)returns a list of(element, count)tuples sorted from most to least frequent.- Unpack each tuple in a
forloop:for item, count in counter.most_common(3):to print the results cleanly.
▼ Solution & Explanation
Explanation:
Counter(items): Iterates through the list once and builds a frequency dictionary. For this input it produces{'apple': 4, 'banana': 3, 'cherry': 2, 'date': 1}..most_common(3): Returns the top 3 elements as a list of(element, count)tuples ordered from highest to lowest count. Calling it with no argument returns all elements sorted by frequency. Internally it usesheapq.nlargestfor efficiency on large counters.- Tuple unpacking in the loop: Writing
for item, count in ...unpacks each two-element tuple directly into named variables, producing cleaner code than accessingpair[0]andpair[1].
Exercise 3: Subtract Two Counters
Problem Statement: Write a Python program that creates two Counter objects from two lists and subtracts one from the other to find the difference in element counts, then prints only the elements with a positive remaining count.
Purpose: Counter arithmetic is useful in inventory management, stock reconciliation, and diff-checking tools where you need to know what remains after removing a set of items from a collection. This exercise highlights the difference between the - operator and the .subtract() method, which handle negative results differently.
Given Input: stock = ["apple", "apple", "apple", "banana", "banana", "cherry"] and sold = ["apple", "apple", "banana", "cherry", "cherry"]
Expected Output: apple: 1, banana: 1 (items with positive remaining count only)
▼ Hint
- Create a
Counterfor each list, then use the-operator:remaining = stock_counter - sold_counter. - The
-operator automatically drops elements with zero or negative counts from the result. Use.subtract()instead if you want to keep negative values. - Iterate over the result with
.items()to print each remaining element and its count.
▼ Solution & Explanation
Explanation:
stock_counter - sold_counter: Subtracts counts element by element and returns a newCountercontaining only those elements whose resulting count is greater than zero. Elements that go to zero or below are silently excluded from the result..subtract(sold_counter): Modifies the counter in place and retains all elements regardless of sign. This makes zero and negative counts visible, which is useful when you need to flag overselling or shortfalls rather than simply drop them.- Key distinction: Use
-when you want a clean positive-only result (e.g. remaining inventory). Use.subtract()when you need to audit deficits (e.g. items sold that were never in stock, which appear as negative counts).
Exercise 4: Character Frequency in a String
Problem Statement: Write a Python program that uses Counter to count the frequency of every character in a given string, then prints only the alphabetic characters sorted from most to least frequent.
Purpose: Character frequency analysis is foundational in cryptography, compression algorithms, language detection, and data validation. This exercise also reinforces filtering and sorting a Counter result, skills that extend naturally to any frequency-ranked output.
Given Input: text = "hello world"
Expected Output: Alphabetic characters sorted by frequency descending, e.g. l: 3, o: 2, h: 1, e: 1, w: 1, r: 1, d: 1
▼ Hint
- Pass the string directly to
Counter()– it iterates over individual characters automatically. - Filter out non-alphabetic characters by checking
char.isalpha()when iterating over the results. - Use
.most_common()with no argument to get all characters sorted by frequency, then apply the alphabetic filter during iteration.
▼ Solution & Explanation
Explanation:
Counter(text): When passed a string,Countertreats each character as a separate element and counts its occurrences. The space in"hello world"is also counted but filtered out in the next step..most_common(): Called with no argument, it returns all elements sorted from highest to lowest count. This ordering is preserved as we iterate, so the filter does not disturb the frequency-descending sequence.char.isalpha(): ReturnsTruefor letters only, excluding spaces, digits, and punctuation. Applying this filter inside the loop keeps the counting step clean and the display step responsible for what gets shown.
Exercise 5: Combine Counters with Addition
Problem Statement: Write a Python program that creates two Counter objects representing separate item inventories and combines them using the + operator to produce a single merged total inventory.
Purpose: Merging frequency maps is a common operation when aggregating data from multiple sources – such as combining sales figures from different stores, merging word counts from multiple documents, or consolidating log entries. The + operator on Counter handles this cleanly without manual looping.
Given Input: warehouse_a = Counter({"apple": 50, "banana": 30, "cherry": 20}) and warehouse_b = Counter({"banana": 40, "cherry": 10, "date": 60})
Expected Output: apple: 50, banana: 70, cherry: 30, date: 60
▼ Hint
- You can initialise a
Counterdirectly from a dictionary by passing the dict as the argument:Counter({"apple": 50, ...}). - Use the
+operator between twoCounterobjects to add counts for matching keys and include keys that appear in only one of the two counters. - Use
sorted(combined.items())to print the merged inventory in alphabetical order for a clean output.
▼ Solution & Explanation
Explanation:
Counter({"apple": 50, ...}): Initialising from a dictionary is useful when you already have counts available rather than a raw iterable to count from. The dict keys becomeCounterkeys and the integer values become their counts.warehouse_a + warehouse_b: Adds counts for keys present in both counters and carries over keys that exist in only one. This produces a newCounterwithout modifying either original – unlike.update(), which modifies the counter in place.sorted(combined.items()): Sorts the key-value pairs alphabetically by key before printing. Without this, the order follows insertion sequence, which may vary. Sorting produces consistent, predictable output regardless of how the counters were constructed.
Exercise 6: Group Words by First Letter
Problem Statement: Write a Python program that takes a list of words and uses defaultdict(list) to group them alphabetically by their first letter, then prints each group.
Purpose: Grouping items by a shared attribute is one of the most common data transformation tasks in programming. Using defaultdict(list) eliminates the need for repetitive if key not in dict checks before appending, resulting in cleaner and more readable grouping logic.
Given Input: words = ["apple", "avocado", "banana", "blueberry", "cherry", "apricot", "cranberry", "bluebell"]
Expected Output: Each letter printed as a heading followed by the words starting with that letter, e.g. a: ['apple', 'avocado', 'apricot']
▼ Hint
- Import
defaultdictfromcollectionsand create one withdefaultdict(list). - Loop over the word list and use
word[0].lower()as the key to access the group, then.append(word)to add the word to it. - Iterate over
sorted(grouped.items())when printing to display groups in alphabetical order.
▼ Solution & Explanation
Explanation:
defaultdict(list): Creates a dictionary that automatically initialises any missing key with an empty list when it is first accessed. This removes the need for the boilerplate patternif key not in d: d[key] = []before every.append()call.word[0].lower(): Extracts the first character of the word and lowercases it to use as the grouping key. This ensures that words starting with uppercase letters are grouped together with their lowercase equivalents.sorted(grouped.items()): Sorts the dictionary entries by key so the output appears in alphabetical order. Sincedefaultdictinherits fromdict, its.items()method works identically to a regular dictionary.
Exercise 7: Count Occurrences Without KeyError
Problem Statement: Write a Python program that counts the occurrences of each item in a list using defaultdict(int), without any manual initialisation of keys, and prints each item with its count.
Purpose: Before Counter and defaultdict existed, counting with a plain dictionary required checking for key existence on every update. This exercise shows how defaultdict(int) solves that cleanly and helps you understand the underlying mechanism that Counter itself builds upon.
Given Input: colours = ["red", "blue", "red", "green", "blue", "blue", "red", "yellow"]
Expected Output: red: 3, blue: 3, green: 1, yellow: 1
▼ Hint
- Create a
defaultdict(int)– when a missing key is accessed, it automatically initialises it to0(the default value ofint()). - Loop over the list and increment each key with
counts[item] += 1– noifcheck needed. - Try rewriting the same logic with a plain
dictto appreciate whatdefaultdictsaves you from writing.
▼ Solution & Explanation
Explanation:
defaultdict(int): The argumentintis a callable that is invoked with no arguments whenever a missing key is accessed. Sinceint()returns0, every new key automatically starts at zero, making the+= 1increment safe on the very first access.counts[colour] += 1: On the first occurrence of a colour this reads the auto-initialised0and stores1. On subsequent occurrences it increments the existing count normally. There is no risk of aKeyErrorat any point.- Relationship to
Counter:Counteris essentially a specialiseddefaultdict(int)with extra methods like.most_common()and arithmetic operators added on top. Understandingdefaultdict(int)gives you insight into howCounterworks internally.
Exercise 8: Build an Adjacency List
Problem Statement: Write a Python program that reads a list of directed edge pairs and uses defaultdict(list) to build and print an adjacency list representation of the graph.
Purpose: The adjacency list is the most common way to represent graphs in software. It appears in network routing, social graph analysis, dependency resolution, and pathfinding algorithms. Using defaultdict(list) makes building one from a raw edge list concise and free of key-existence boilerplate.
Given Input: edges = [("A", "B"), ("A", "C"), ("B", "D"), ("C", "D"), ("D", "E")]
Expected Output: Each node printed with its list of neighbours, e.g. A: ['B', 'C'], B: ['D'], C: ['D'], D: ['E']
▼ Hint
- Create a
defaultdict(list)calledgraph. - Loop over the edge pairs, unpacking each tuple as
src, dst, and appenddsttograph[src]. - For an undirected graph, also append
srctograph[dst]inside the same loop iteration.
▼ Solution & Explanation
Explanation:
for src, dst in edges: Unpacks each two-element tuple from the edge list directly into named variables. This is cleaner and more expressive than accessingedge[0]andedge[1]inside the loop body.graph[src].append(dst): Becausegraphis adefaultdict(list), accessing a node that has not been seen before automatically creates an empty list for it. The destination node is then appended without any prior initialisation check.- Directed vs undirected: In a directed graph, an edge from A to B does not imply an edge from B to A, so only one
.append()is needed per edge. In an undirected graph, both directions are recorded by adding a second.append()that insertssrcintograph[dst]as well.
Exercise 9: Nested defaultdict
Problem Statement: Write a Python program that uses a nested defaultdict to store a two-level dictionary representing departments and their employee names, then prints the full structure without manually creating any inner dictionaries.
Purpose: Nested data grouping is a frequent requirement in reporting, configuration management, and data pipelines. A nested defaultdict eliminates the need to check whether an inner dictionary exists before writing to it, making multi-level data assembly significantly cleaner than with plain dictionaries.
Given Input: A list of (department, employee) tuples: [("Engineering", "Alice"), ("Marketing", "Bob"), ("Engineering", "Charlie"), ("Marketing", "Diana"), ("HR", "Eve")]
Expected Output: Each department printed as a heading followed by its employees and their placeholder data, e.g. Engineering: {'Alice': [], 'Charlie': []}
▼ Hint
- Use
defaultdict(lambda: defaultdict(list))to create a two-level structure where the outer keys are departments and the inner keys are employee names mapping to lists. - Alternatively, use
defaultdict(dict)for the outer level if you only need a plain inner dictionary per department. - Access a nested key directly with
structure[dept][employee]– both levels auto-initialise on first access with noKeyError.
▼ Solution & Explanation
Explanation:
defaultdict(lambda: defaultdict(list)): The outerdefaultdictuses alambdaas its default factory so that each missing outer key is initialised with a brand-new innerdefaultdict(list). A plaindefaultdict(defaultdict(list))would not work because it would share the same inner instance across all outer keys.org[dept][employee]: Accessing a two-level key triggers the default factory at each level in sequence. The outer access creates the innerdefaultdict(list)if the department is new. The inner access creates an empty list if the employee name is new. Noifchecks or pre-initialisation are needed.- Plain dict equivalent: Without
defaultdictyou would writeif dept not in org: org[dept] = {}followed byif employee not in org[dept]: org[dept][employee] = []before every insert. The nesteddefaultdictreplaces all of that with a single line.
Exercise 10: defaultdict with set
Problem Statement: Write a Python program that takes a list of (student, subject) pairs and uses defaultdict(set) to collect all unique subjects each student is enrolled in, then prints each student’s subject list.
Purpose: Using a set as the default value automatically deduplicates entries, which is ideal when the source data may contain repeated pairs. This pattern appears in access control systems, tag collections, and any scenario where you need to associate a key with a unique group of values rather than an ordered list.
Given Input: enrolments = [("Alice", "Maths"), ("Bob", "Science"), ("Alice", "Science"), ("Alice", "Maths"), ("Bob", "Maths"), ("Charlie", "Science")]
Expected Output: Each student with their unique subjects, e.g. Alice: {'Maths', 'Science'}, Bob: {'Science', 'Maths'}, Charlie: {'Science'}
▼ Hint
- Create a
defaultdict(set)and use.add()instead of.append()– sets useaddrather thanappend. - Duplicate
(student, subject)pairs will be silently ignored because adding an existing element to a set has no effect. - Use
sorted(subjects)when printing to display subjects in a consistent alphabetical order, since sets are unordered.
▼ Solution & Explanation
Explanation:
defaultdict(set): Each missing key is initialised with an emptyset()on first access. This means the first call tosubjects_by_student["Alice"].add("Maths")creates the set and adds the value in a single step, with no prior initialisation required..add(subject): The set method for inserting an element. If the element is already present, the call is a no-op – the set remains unchanged and no error is raised. This is the key reason to preferdefaultdict(set)overdefaultdict(list)when uniqueness is required.sorted(subjects): Sets have no guaranteed iteration order. Wrapping the set insorted()converts it to an alphabetically ordered list for display, giving consistent output regardless of insertion order or Python version.
Exercise 11: Preserve Insertion Order
Problem Statement: Write a Python program that creates an OrderedDict from a list of key-value pairs and demonstrates that iteration always reflects the original insertion order, even after accessing individual keys.
Purpose: While Python 3.7 and later guarantee insertion order for regular dicts, OrderedDict remains relevant because it makes the ordering guarantee explicit in the code’s intent, supports additional order-aware methods like move_to_end(), and behaves differently in equality comparisons – as shown in Exercise 14. This exercise builds the foundation for the more advanced OrderedDict exercises that follow.
Given Input: pairs = [("banana", 3), ("apple", 5), ("cherry", 1), ("date", 8), ("elderberry", 2)]
Expected Output: Keys and values printed in insertion order: banana: 3, apple: 5, cherry: 1, date: 8, elderberry: 2
▼ Hint
- Import
OrderedDictfromcollectionsand pass the list of tuples directly to its constructor. - Iterate using
.items()to confirm that the output order matches the input order of the pairs list. - Access a key by name (e.g.
od["apple"]) and then iterate again to confirm the order has not changed.
▼ Solution & Explanation
Explanation:
OrderedDict(pairs): Accepts any iterable of(key, value)pairs and stores them in the exact order they were provided. Subsequent insertions are appended to the end of the internal order, and re-assigning an existing key updates its value without changing its position.- Order after access: Unlike some cache or lookup structures, reading a key in an
OrderedDictdoes not move it. The order only changes when you explicitly callmove_to_end()or delete and re-insert a key. - When to prefer
OrderedDictoverdict: UseOrderedDictwhen order-aware operations likemove_to_end()andpopitem(last=False)are needed, when equality checks must consider order (see Exercise 14), or when the code needs to be explicit about the fact that ordering is a deliberate design choice rather than an implementation detail.
Exercise 12: Move to End
Problem Statement: Write a Python program that creates an OrderedDict of tasks, then uses move_to_end() to promote one task to the front and demote another to the back, printing the order before and after each operation.
Purpose: Repositioning elements within an ordered collection is central to priority queues, recently-used lists, and scheduling systems. move_to_end() performs this in-place in O(1) time, making it far more efficient than deleting and re-inserting a key in a plain dictionary.
Given Input: tasks = [("task_1", "Write tests"), ("task_2", "Fix bug"), ("task_3", "Deploy"), ("task_4", "Code review"), ("task_5", "Update docs")]
Expected Output: The task list printed three times: original order, after moving task_3 to the front, and after moving task_1 to the back.
▼ Hint
- Call
od.move_to_end(key)to move a key to the last position (the default behaviour). - Pass
last=Falseto move a key to the first position:od.move_to_end(key, last=False). - Write a small helper function to print the current order so you can call it before and after each operation without repeating the loop code.
▼ Solution & Explanation
Explanation:
move_to_end(key, last=True): Repositions the specified key to the last position in the ordered sequence. This is the default behaviour whenlastis omitted. The operation is performed in O(1) time becauseOrderedDictuses a doubly-linked list internally to track order.move_to_end(key, last=False): Moves the key to the first position instead. This is the idiomatic way to promote an item to the top of an ordered structure, such as marking a task as urgent or moving a recently accessed item to the front of a cache.- Helper function: Extracting the print loop into a
print_tasks()function removes repetition and keeps the main logic focused on the move operations. This is a good general habit when the same display logic is needed multiple times in a script.
Exercise 13: Implement a Simple LRU Cache
Problem Statement: Write a Python program that implements a basic Least Recently Used (LRU) cache using OrderedDict. The cache should store up to a fixed number of entries and automatically evict the least recently used item when the limit is exceeded.
Purpose: LRU caches are used in web browsers, database query caches, CDN layers, and CPU memory management to keep frequently accessed data in fast storage while discarding stale entries. Building one with OrderedDict is a classic interview question that demonstrates mastery of both data structure selection and cache eviction logic.
Given Input: A cache with capacity = 3, followed by a sequence of get and put operations that triggers at least one eviction.
Expected Output: Cache state printed after each operation showing which items are present and which was evicted.
▼ Hint
- Store entries in an
OrderedDictwhere the least recently used item is always at the front (index 0) and the most recently used is at the back. - On every
get, move the accessed key to the end usingmove_to_end(key)to mark it as most recently used. - On every
put, if the cache is full, evict the first item usingpopitem(last=False)before inserting the new entry.
▼ Solution & Explanation
Explanation:
move_to_end(key)on get: Every time an existing key is read, it is moved to the back of theOrderedDictto record that it was the most recently used item. This keeps the front of the dict permanently occupied by the least recently used entry, making eviction trivial.popitem(last=False): Removes and returns the first item in theOrderedDict– the least recently used entry. This is the eviction step.last=Falseis essential here: the defaultlast=Truewould remove the most recently used item instead, which is the opposite of LRU behaviour.- Why
OrderedDictoverdict: Regular dicts in Python 3.7 and later preserve insertion order but do not exposemove_to_end()or order-awarepopitem(last=False). Both are essential for efficient LRU logic, makingOrderedDictthe correct tool for this pattern.
Exercise 14: Compare Two OrderedDicts
Problem Statement: Write a Python program that demonstrates the difference in equality behaviour between two regular dicts and two OrderedDict objects that contain the same keys and values but in different insertion orders.
Purpose: Understanding how equality works for OrderedDict versus dict prevents subtle bugs in code that compares configurations, serialised records, or cached results where insertion order may differ. This is one of the most commonly misunderstood behaviours of OrderedDict.
Given Input: Two dictionaries and two OrderedDict objects, each containing {"a": 1, "b": 2, "c": 3} but constructed in different key orders.
Expected Output: dict equality: True and OrderedDict equality: False
▼ Hint
- Create two plain dicts with the same keys and values but constructed in different orders, then compare with
==. - Do the same with
OrderedDictand compare again – the result will differ. - Also compare an
OrderedDictwith a plaindictcontaining the same data to see how cross-type equality behaves.
▼ Solution & Explanation
Explanation:
- Plain dict equality: Two dicts are equal if they contain the same key-value pairs, regardless of the order in which keys were inserted. This is why
{"a": 1, "b": 2} == {"b": 2, "a": 1}evaluates toTrue. - OrderedDict equality: Two
OrderedDictobjects are equal only if they contain the same key-value pairs in the same insertion order. This makesOrderedDictequality stricter than plain dict equality, which is intentional for order-sensitive comparisons. - Cross-type equality: When comparing an
OrderedDictwith a plaindict, Python uses the plain dict’s equality logic, which ignores order. SoOrderedDict([("a", 1), ("b", 2)]) == {"b": 2, "a": 1}returnsTrue. This asymmetry can be surprising and is worth knowing to avoid incorrect test assertions.
Exercise 15: Reverse an OrderedDict
Problem Statement: Write a Python program that creates an OrderedDict and iterates over it in reverse order using reversed(), printing key-value pairs from last inserted to first inserted.
Purpose: Reverse iteration over an ordered collection is useful when displaying recent history, processing a stack of operations in reverse, or presenting results in descending insertion sequence. OrderedDict supports reversed() directly, whereas reversing a plain dict requires converting it to a list first in older Python versions.
Given Input: steps = [("step_1", "Load data"), ("step_2", "Clean data"), ("step_3", "Analyse"), ("step_4", "Visualise"), ("step_5", "Export")]
Expected Output: Steps printed in reverse insertion order from step_5 down to step_1.
▼ Hint
- Use
reversed(od)to get an iterator over the keys in reverse order, then access each value withod[key]. - Alternatively, use
reversed(od.items())in Python 3.8 and later to iterate over key-value pairs directly in reverse. - Print the steps in both forward and reverse order to clearly show the contrast.
▼ Solution & Explanation
Explanation:
reversed(steps.items()): Returns a reverse iterator over the key-value pairs of theOrderedDict. Support forreversed()on.items(),.keys(), and.values()was added in Python 3.8. In earlier versions, the workaround islist(steps.items())[::-1].- No copy needed:
reversed()on anOrderedDictproduces a lazy iterator rather than creating a reversed copy of the data. This is memory-efficient for large dictionaries where a full copy would be wasteful. - Practical uses: Reverse iteration is useful for undo stacks (processing the most recent action first), audit logs (showing newest entries at the top), and pipeline rollback logic where later steps must be reversed before earlier ones.
Exercise 16: Basic deque Operations
Problem Statement: Write a Python program that creates a deque from a list and demonstrates the four core operations: append(), appendleft(), pop(), and popleft(), printing the deque state after each operation.
Purpose: A deque (double-ended queue) supports O(1) insertions and removals from both ends, unlike a list where inserting or removing from the front is O(n). Understanding the four core operations is essential before using deque in queues, stacks, sliding windows, and history buffers covered in subsequent exercises.
Given Input: initial = [2, 3, 4]
Expected Output: The deque state printed after each of the six operations showing how items enter and leave from both ends.
▼ Hint
- Import
dequefromcollectionsand initialise it withdeque([2, 3, 4]). append(x)adds to the right end andappendleft(x)adds to the left end.pop()removes from the right end andpopleft()removes from the left end – both return the removed element.
▼ Solution & Explanation
Explanation:
append()andappendleft(): Both run in O(1) time by design. In contrast,list.insert(0, x)for front insertion is O(n) because every existing element must be shifted one position to the right. This makesdequethe correct choice whenever frequent front insertions are needed.pop()andpopleft(): Both also run in O(1) time.popleft()is the efficient equivalent oflist.pop(0), which is O(n) for lists. Both raise anIndexErrorif called on an empty deque.extendleft(iterable): Adds each item from the iterable to the left end one at a time, which reverses the order of the iterable in the deque. Soextendleft([0, -1])first adds0to the left, then adds-1to the left, resulting in-1being the leftmost element.
Exercise 17: Implement a Queue (FIFO)
Problem Statement: Write a Python program that simulates a customer service queue using a deque, where customers join at the back and are served from the front, printing the queue state after each operation.
Purpose: A queue (First In, First Out) is one of the most fundamental data structures in computer science. It models real-world waiting lines, task schedulers, breadth-first search frontiers, and message buffers. Using deque instead of a list gives O(1) performance at both ends, making it the correct tool for queue implementation in Python.
Given Input: A sequence of customer arrivals and service operations applied to an initially empty queue.
Expected Output: The queue state printed after each arrival and each service event, showing customers entering at the back and leaving from the front.
▼ Hint
- Use
deque.append(item)to enqueue a customer at the back of the line. - Use
deque.popleft()to dequeue and serve the customer at the front. - Check
len(queue) > 0or use a boolean check on the deque before callingpopleft()to avoid anIndexErroron an empty queue.
▼ Solution & Explanation
Explanation:
queue.append(customer): Adds the new customer to the right end of the deque. Becausedequeperforms this in O(1) time, it is more efficient thanlist.append()for this use case – though both are O(1) at the right end. The real advantage shows when removing from the front.queue.popleft(): Removes and returns the leftmost element in O(1) time. The equivalent operation on a list –list.pop(0)– is O(n) because every remaining element must be shifted one position left. For large queues this difference is significant.- Empty check with
if queue: An emptydequeevaluates toFalsein a boolean context, soif queue:is the idiomatic way to guard against callingpopleft()on an empty structure without an explicit length check.
Exercise 18: Implement a Stack (LIFO)
Problem Statement: Write a Python program that uses a deque to implement a stack where items are pushed onto and popped from the same end, simulating a browser back-navigation history.
Purpose: A stack (Last In, First Out) underpins function call frames, expression evaluation, undo systems, and depth-first search. Although a plain Python list supports stack operations, using deque makes the intent explicit and consistent with the queue implementation, reinforcing the idea that deque can model both structures depending on which end is used.
Given Input: A sequence of page visits and back-navigation events applied to an initially empty history stack.
Expected Output: The stack state printed after each push and pop, showing pages added and removed from the same end.
▼ Hint
- Use
deque.append(item)to push a page onto the top of the stack (right end). - Use
deque.pop()to pop the most recently visited page from the top (right end). - Use
deque[-1]to peek at the top item without removing it, which represents the current page.
▼ Solution & Explanation
Explanation:
history.append(page)andhistory.pop(): Both operate on the right end of the deque, which is the top of the stack. Using the same end for both push and pop is what defines LIFO behaviour – the last item pushed is always the first to be popped.history[-1]: Accesses the rightmost element without removing it. This is a peek operation – it tells you what is currently on top of the stack without changing the structure. It raises anIndexErroron an empty deque, so always guard with a length check when the deque might be empty.- Stack vs queue summary: The only structural difference between this stack and the queue in Exercise 17 is which end is used for removal. Queue uses
appendto add andpopleftto remove (opposite ends). Stack usesappendto add andpopto remove (same end). Thedequestructure supports both patterns equally well.
Exercise 19: Rotating a deque
Problem Statement: Write a Python program that creates a deque of tasks and uses deque.rotate(n) to shift the elements right by 2 positions and then left by 2 positions, printing the state after each rotation.
Purpose: Rotation is useful in round-robin scheduling, circular buffer simulations, and any algorithm that needs to cycle through a fixed collection repeatedly. deque.rotate() performs the operation in O(n) time in a single method call, without needing to manually slice, concatenate, or reassign elements.
Given Input: tasks = ["Design", "Develop", "Test", "Review", "Deploy"]
Expected Output: Task list printed in original order, after rotating right by 2, and after rotating left by 2.
▼ Hint
- Call
dq.rotate(2)to shift all elements two positions to the right – elements that fall off the right end wrap around to the left. - Call
dq.rotate(-2)to shift two positions to the left – elements that fall off the left end wrap around to the right. - Rotating by
nand then by-nrestores the original order, so you can verify correctness by chaining both calls.
▼ Solution & Explanation
Explanation:
rotate(n)with positive n: Moves elements from the right end to the left end. Withn=2on["Design", "Develop", "Test", "Review", "Deploy"], the last two elements"Review"and"Deploy"wrap to the front, producing["Review", "Deploy", "Design", "Develop", "Test"].rotate(-n)with negative n: Moves elements from the left end to the right end. Applyingrotate(-2)afterrotate(2)exactly reverses the first rotation, restoring the original order. This symmetry makes rotation easy to reason about and reverse.- Round-robin scheduling: Calling
rotate(-1)after processingtasks[0]moves the just-processed task to the back and brings the next task to the front. This is the canonical way to implement a circular task scheduler with adeque– no index arithmetic or modulo operations required.
Exercise 20: Bounded deque for Recent History
Problem Statement: Write a Python program that creates a deque with maxlen=5 to simulate a recent browsing history buffer, demonstrating that adding a sixth item automatically discards the oldest entry.
Purpose: A bounded deque is the simplest way to maintain a fixed-size sliding window of recent events without manually checking length or removing old entries. It appears in terminal command history, log tail buffers, moving average calculations, and any feature that shows “last N items” to the user.
Given Input: A sequence of six page URLs added one at a time to a deque(maxlen=5).
Expected Output: History buffer state after each addition, showing the oldest URL being dropped when the sixth is added.
▼ Hint
- Create the deque with
deque(maxlen=5)– no extra eviction logic is needed; Python handles it automatically. - Use
dq.append(item)to add new items to the right. When the deque is full, the leftmost (oldest) item is silently dropped before the new item is added. - Check
dq.maxlento confirm the configured limit andlen(dq)to see the current count at any time.
▼ Solution & Explanation
Explanation:
deque(maxlen=5): Sets a fixed upper bound on the number of elements the deque can hold. Once the deque reaches its maximum length, any newappend()call automatically removes the element from the opposite end before inserting the new one. No manual length checks or pop calls are needed.- Automatic eviction direction: When using
append()to add to the right on a full deque, the leftmost (oldest) element is dropped. Conversely, when usingappendleft(), the rightmost element is dropped. The eviction always occurs at the end opposite to the insertion point. dq.maxlen: A read-only attribute that stores the configured maximum size. It isNonefor unbounded deques. Checking it at runtime lets code behave differently depending on whether a size limit was set, which is useful when writing reusable buffer utilities.
Exercise 21: Create a namedtuple
Problem Statement: Write a Python program that defines a Point namedtuple with fields x and y, creates two instances, and accesses their coordinates using both attribute access and positional index access.
Purpose: A namedtuple gives a plain tuple named fields, making code that passes coordinate pairs, RGB values, or simple records significantly more readable than using raw index numbers. It is immutable, memory-efficient, and fully compatible with code that expects regular tuples, making it a zero-overhead upgrade over anonymous tuples.
Given Input: Two points – Point(3, 7) and Point(10, 4).
Expected Output: Coordinates accessed by attribute name and by index, plus the distance between the two points.
▼ Hint
- Import
namedtuplefromcollectionsand define the type withPoint = namedtuple("Point", ["x", "y"]). - Access fields by name (
p.x,p.y) or by index (p[0],p[1]) – both work on the same instance. - Use the
mathmodule to calculate Euclidean distance:math.sqrt((p2.x - p1.x)**2 + (p2.y - p1.y)**2).
▼ Solution & Explanation
Explanation:
namedtuple("Point", ["x", "y"]): Creates a new class calledPointthat is a subclass oftuple. The first argument is the class name (used inreproutput), and the second is the list of field names. The field names can also be passed as a single space-separated string:"x y".- Attribute vs index access: Both
p1.xandp1[0]return the same value. Named access is preferred in application code for readability. Index access is useful when passing the namedtuple to code that expects a plain tuple and uses positional indexing. - Immutability: Like regular tuples, namedtuple instances are immutable. Attempting
p1.x = 10raises anAttributeError. To create a modified version, use_replace()as demonstrated in Exercise 24.
Exercise 22: namedtuple as a Lightweight Record
Problem Statement: Write a Python program that defines an Employee namedtuple with fields name, department, and salary, creates a list of employee records, and prints all employees belonging to a specific department.
Purpose: Namedtuples are an excellent lightweight alternative to full classes or dictionaries for storing structured records when no methods or mutability are needed. This pattern is widely used for database row representations, CSV record models, API response objects, and configuration entries where clarity and low overhead matter.
Given Input: A list of five Employee records across three departments. Filter for "Engineering".
Expected Output: Name and salary of every employee in the Engineering department, one per line.
▼ Hint
- Define the namedtuple with
Employee = namedtuple("Employee", ["name", "department", "salary"]). - Use a list comprehension to filter employees:
[e for e in employees if e.department == "Engineering"]. - Access fields by name in the print statement for readable output:
e.nameande.salary.
▼ Solution & Explanation
Explanation:
- Named field access in filtering:
e.department == target_deptreads clearly as business logic. The equivalent index-based checke[1] == target_deptwould work but obscures the intent, especially as the number of fields grows. - List comprehension filtering:
[e for e in employees if e.department == target_dept]produces a filtered list in one line. Because namedtuple instances are immutable tuples, the filtered list holds references to the originals with no copying overhead. - Namedtuple vs dataclass: For read-only record types with no methods,
namedtupleis more memory-efficient than adataclassbecause it stores data in a tuple rather than a per-instance__dict__. If mutability or methods are needed, adataclassis the better choice.
Exercise 23: Convert namedtuple to Dictionary
Problem Statement: Write a Python program that creates a namedtuple instance and converts it to a regular dictionary using the _asdict() method, then demonstrates how to use the resulting dictionary for JSON serialisation.
Purpose: Namedtuples cannot be directly serialised to JSON because json.dumps() does not recognise them as dictionaries. Converting to a dict first is the standard bridge between the lightweight namedtuple record model and JSON-based APIs, configuration files, or database serialisation layers.
Given Input: A Product namedtuple with fields name, category, price, and in_stock.
Expected Output: The namedtuple printed as a dict, then as a JSON string.
▼ Hint
- Call
instance._asdict()on any namedtuple instance to get anOrderedDict(Python 3.7 and earlier) or a regulardict(Python 3.8 and later) with field names as keys. - Pass the resulting dict to
json.dumps(d, indent=2)to produce a formatted JSON string. - You can also reconstruct a namedtuple from a dict using the
**unpacking operator:Product(**d).
▼ Solution & Explanation
Explanation:
_asdict(): Returns a dictionary mapping each field name to its value. The leading underscore is a Python convention indicating it is part of the namedtuple API rather than a user-defined field, which prevents naming conflicts. In Python 3.8 and later the return type is a plaindict; in earlier versions it is anOrderedDict.json.dumps(product_dict, indent=2): Serialises the plain dictionary to a JSON-formatted string. Passing a namedtuple directly tojson.dumps()would raise aTypeErrorbecause the JSON encoder only handles dicts, lists, strings, numbers, and booleans by default – not tuples with named fields.Product(**product_dict): The double-star unpacking operator spreads the dictionary key-value pairs as keyword arguments to theProductconstructor. This round-trip pattern – namedtuple to dict, then dict back to namedtuple – is the standard way to serialise and deserialise namedtuple records.
Exercise 24: Replace a Field Value
Problem Statement: Write a Python program that creates a namedtuple instance representing a product listing and uses the _replace() method to produce updated copies with changed field values, without modifying the original instance.
Purpose: Because namedtuples are immutable, _replace() is the correct way to create a modified version of an existing record – analogous to using dataclasses.replace() for dataclasses or spread syntax in JavaScript. This pattern is common in functional programming workflows where data is never mutated in place.
Given Input: A Listing namedtuple with fields title, price, available, and rating.
Expected Output: Original listing printed, then two modified copies with updated fields, confirming the original is unchanged.
▼ Hint
- Call
instance._replace(field=new_value)to create a new namedtuple instance with the specified field changed and all other fields copied from the original. - You can replace multiple fields in a single call:
instance._replace(price=49.99, available=False). - After calling
_replace(), verify that the original instance is unchanged to reinforce that namedtuples are immutable.
▼ Solution & Explanation
Explanation:
_replace(price=59.99): Creates and returns a brand-new namedtuple instance of the same type withpriceset to59.99and all other fields copied fromoriginal. The original instance is never touched. The leading underscore follows the same naming convention as_asdict()– it is part of the namedtuple API, not a user field.- Multiple fields in one call:
_replace(available=False, rating=4.5)updates two fields simultaneously in a single call, producing a new instance in one step rather than chaining two separate replacements. original is discounted: Theisoperator checks object identity rather than equality. Because_replace()always creates a new object, this returnsFalseeven when the field values are identical. This confirms that no in-place mutation occurred.
Exercise 25: namedtuple with Default Values
Problem Statement: Write a Python program that defines a Config namedtuple using the defaults parameter so that some fields have fallback values and can be omitted at instantiation, then creates instances with and without the optional fields.
Purpose: Default values make namedtuples practical for configuration objects, optional record fields, and function return types where some fields may not always be populated. This feature was added in Python 3.6.1 and removes the need to subclass or use __new__ tricks to achieve optional fields.
Given Input: A Config namedtuple with required fields host and port, and optional fields debug (default False), timeout (default 30), and max_retries (default 3).
Expected Output: A fully specified config and a minimal config printed side by side, showing default values filling in the omitted fields.
▼ Hint
- Pass a tuple of default values to the
defaultsparameter ofnamedtuple(). Defaults apply to the rightmost fields first, so required fields must come before optional ones. - Check
Config._field_defaultsto see which fields have defaults and what their values are. - Create one instance providing all fields and one providing only the required fields, then print both to verify the defaults are applied correctly.
▼ Solution & Explanation
Explanation:
defaults=[False, 30, 3]: Provides default values for the last three fields in order:debug=False,timeout=30, andmax_retries=3. Defaults always fill from the right, so the number of defaults cannot exceed the number of fields, and required fields (those without defaults) must appear first in the field list.Config._field_defaults: A dictionary mapping each field name that has a default to its default value. It only contains entries for fields with defaults, not for required fields. This is useful for introspection, documentation generation, and testing.- Namedtuple defaults vs dataclass defaults: The
defaultsparameter achieves the same result as default values in adataclassfield definition, but namedtuple instances remain immutable tuples with lower memory overhead. For simple read-only configuration records, a namedtuple with defaults is often the most concise solution available.

Leave a Reply