Python Module 5
Built-in data structures: lists, dicts, sets and more interview questions
Interview questions on “Built-in data structures: lists, dicts, sets and more”: warm-ups, then what each harder question tests and how a good answer starts.
Warm-up questions
Short questions with short answers, each answered in full.
-
Fresher
What is the difference between append() and extend() on a Python list?
Answered in the lesson Lists: create, index, slice and modify
-
Fresher to mid
How does a shallow copy of a nested list differ from a deep copy?
Answered in the lesson References, aliasing, shallow and deep copies
-
Fresher
What is the difference between sorted() and list.sort()?
Answered in the lesson Sorting with key functions and stability
-
Fresher to mid
Why can a tuple be a dictionary key when a list cannot?
-
Fresher
What happens when you look up a missing key with d[key], d.get(key) and d.setdefault(key, [])?
-
Fresher to mid
Why is x in a_set usually faster than x in a_list, and when is it not?
Answered in the lesson Sets and frozensets for membership and maths
-
Fresher
What is the difference between [x for x in xs if x > 0] and [x if x > 0 else 0 for x in xs]?
-
Fresher
On a defaultdict(list), what is the difference between d[key] and d.get(key) when the key is missing?
Answered in the lesson collections: deque, Counter, defaultdict and more
-
Fresher
How does heapq represent a heap, and where is its smallest item?
Answered in the lesson heapq, bisect and array for efficient data
-
Fresher
For the string "नमस्ते", what is the difference between len(s) and len(s.encode("utf-8"))?
Answered in the lesson Text vs bytes: Unicode, encodings, bytearray
-
Fresher
Which Python structures would you use for a stack, a queue and a priority queue, and why?
Questions with model answers
What each question tests, the first step of a good answer and the follow-ups to expect.
-
Fresher to mid
A support desk keeps waiting tickets in a Python list, adds new ones with append() and serves the oldest with pop(0). With a few hundred tickets nobody notices; with 200,000 waiting, the service spends most of its CPU time inside pop(0). Explain why, estimate the work, and fix it.
- What it tests
- Knowing that removing from the front of a list moves every other item, estimating the total work, and choosing a structure built for queues.
- First step
- Say what pop(0) does: a CPython list is an array of references, so removing the first one moves every remaining reference one place to the left. Serving n tickets that way moves about n × n ÷ 2 references, some 2 × 10¹⁰ for 200,000 tickets, while a queue that removes from the front in constant time does 200,000 small steps.
- Follow-ups
-
- Urgent tickets must now jump the queue.
- A customer cancels a ticket that is still waiting.
- Several worker threads serve tickets at the same time.
From the lesson Lists: create, index, slice and modify
Model answer, Pro. Model answers open with any pass; prices are in the notice on this page.Opening the model answers with your pass…
Model answers open with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so the model answers can’t be opened at the moment. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 3 minutes
-
Mid to senior
A script appends 10 million floats to a list, one at a time. Why is each append fast on average, although the list has to grow many times, and why does the finished list need about 320 MB when 10 million 8-byte numbers are only 80 MB? How would you cut the memory?
- What it tests
- Dynamic-array growth and amortised constant-time append, what a list really stores, and when to store numbers in an array instead.
- First step
- Split the memory in two: the list holds one 8-byte reference per item plus a few spare slots, about 80 MB, and every float is a separate 24-byte object, about 240 MB more. Then explain growth: when the array is full, CPython allocates a bigger one with room to spare, so the copying happens only now and then.
- Follow-ups
-
- How would you show the growth steps of a list in a few lines of code?
- Would [0.0] * 10_000_000 need the same memory?
- When would you pick array('d') over a list, and when not?
From the lesson Lists: create, index, slice and modify
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 3 minutes
-
Mid to senior
A board game keeps an undo history by calling copy.deepcopy(board) before every move, where the board is a list of 100 lists of 100 cells. A profile shows most of the time inside deepcopy. Why is it slow, and what would you do instead?
- What it tests
- What deepcopy does for each object it meets, measuring copying costs, and choosing an undo design that copies less or nothing.
- First step
- Count the work: the board is 101 lists and 10,100 references, and deepcopy visits every one in Python code, with a memo entry for each list, before every move. Then ask what a move changes: usually one or two cells. Copying the rows with row[:] is far cheaper, and recording only the change is cheaper still.
- Follow-ups
-
- How much faster is copying each row than deepcopy, in your measurements?
- The cells now hold objects with their own nested state.
- How would you undo a move without copying anything?
From the lesson References, aliasing, shallow and deep copies
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 3 minutes
-
Mid to senior
Log records are (timestamp, level, message) tuples whose timestamps are fixed-width ISO 8601 strings. Sort a million of them so that CRITICAL comes first, then ERROR, WARNING and INFO, and within each level the newest first. Write the key, and say what the sort costs.
- What it tests
- Turning a custom category order into numbers, mixing directions when a field cannot be negated, and measuring key-based sorts.
- First step
- Map each level to a rank with a dictionary, CRITICAL 0 to INFO 3. A string timestamp cannot be negated, so either sort in two passes, newest first and then by rank, or make one descending sort whose key is (-rank, timestamp): descending by -rank means ascending by rank, and timestamps come out newest first.
- Follow-ups
-
- Why can ISO 8601 text be sorted as text, and when does that stop working?
- Some records have a level the dictionary does not know, such as DEBUG.
- The records no longer fit in memory.
From the lesson Sorting with key functions and stability
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 4 minutes
-
Mid to senior
An app lists 50,000 customer names from many countries with sorted(names). Users report that every name starting with a small letter comes after all the capitalised ones, and that Émile appears after Zoya. Explain why, and design a sort that people accept. What does your fix cost?
- What it tests
- Code-point order against human collation, casefold() and Unicode normalisation, locale-aware sort keys, and the cost of computing keys.
- First step
- Explain the default: strings compare by code point, so the capitals A to Z (65 to 90) sort before the small letters (97 to 122), and accented letters, from 192 upwards, after both. key=str.casefold fixes case; the order people expect needs a collation key for their language, computed once per name.
- Follow-ups
-
- Why casefold() rather than lower()?
- The same name is stored once with é as one code point and once as e plus a combining accent.
- Users in different countries expect different orders for the same names.
From the lesson Sorting with key functions and stability
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 3 minutes
-
Mid to senior
After t = (1, [2]), the statement t[1] += [3] raises TypeError, yet t is (1, [2, 3]) afterwards. Explain step by step what Python did, whether this is a bug, and how you would design a record that is truly immutable.
- What it tests
- Augmented assignment as separate steps, an immutable container holding mutable contents, and designing records that cannot change.
- First step
- Split the statement the way Python runs it: read t[1], apply += to that object, then store the result back into t[1]. For a list, += extends the list in place and succeeds; storing into a tuple's item is not allowed, so the last step raises after the list has already changed.
- Follow-ups
-
- Would t[1].extend([3]) or t[1] = t[1] + [3] behave differently?
- How can you see the separate steps in the bytecode?
- Where does this behaviour cause trouble in real programs?
From the lesson Tuples, unpacking and named tuples
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 3 minutes
-
Mid to senior
A service passes user records around as plain tuples (id, name, email), unpacked or indexed in about 40 places. Product needs a phone field between name and email. What breaks, and would you move to a NamedTuple, a dataclass or a dict? Compare their safety and memory.
- What it tests
- The fragility of positional records, choosing a record type, migrating call sites safely and the memory each record type needs.
- First step
- List what breaks: every unpacking into three names raises ValueError, which is loud, while every index such as record[2] silently reads the phone instead of the email, which is worse. Then propose named fields, with the new field added at the end or given a default, so positional code keeps working during the migration.
- Follow-ups
-
- How much memory does each option need per record?
- The records are also used as dictionary keys.
- How do you migrate the 40 call sites without a big-bang change?
From the lesson Tuples, unpacking and named tuples
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 4 minutes
-
Mid to senior
A Point class defines __eq__ and __hash__ from its x and y attributes, which can be changed. Points are keys in a dictionary of visited cells. After one point's x is changed, visited[p] raises KeyError and p in visited is False, although the entry is still in the dictionary. Explain, and fix the class.
- What it tests
- How a hash table finds a key, the rule that a key's hash must not change, and making value objects safe to use as keys.
- First step
- Describe the lookup: the dictionary uses hash(key) to decide where to look and then compares candidates with ==. The entry sits where the old hash pointed; after x changes, the new hash sends every lookup somewhere else, so the stored key is never even compared. The class broke the rule that a key's hash never changes.
- Follow-ups
-
- Why does Python make a class unhashable when it defines __eq__ without __hash__?
- How do you find the stranded entry again?
- Would a frozen dataclass fix it completely?
From the lesson Dictionaries: keys, values and views
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 3 minutes
-
Mid to senior
Settings come from defaults, a config file and environment variables, merged with {defaults, file_cfg, **env_cfg}. When the file sets only database.host, the default database.port disappears. Explain why, describe the merge you would write, and compare |, update() and collections.ChainMap for this job.
- What it tests
- Shallow merge semantics, writing a recursive merge and its edge cases, and when a ChainMap view beats a merged copy.
- First step
- Show the cause: unpacking merges top-level keys only, so the whole database dictionary from the file replaces the default one, port included. The fix walks both dictionaries together: where both sides hold a dictionary under the same key, merge those two recursively; anywhere else, the later value wins.
- Follow-ups
-
- Should lists in the settings be merged or replaced?
- How do you keep the merge from changing the defaults?
- When is ChainMap the better tool?
From the lesson Dictionaries: keys, values and views
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 3 minutes
-
Mid to senior
A file of 5 million lines holds one user ID per line, many of them repeated. Write each ID out once, in the order first seen, and report how many lines were repeats. Which structure do you use, how much memory does it need, and what changes if each line is a JSON record instead?
- What it tests
- Streaming de-duplication with a set, estimating memory per item from measurements, and making unhashable records hashable.
- First step
- Stream the file and keep a set of the IDs seen so far: for each line, if the ID is not in the set, write it and add it, otherwise count a repeat. Writing as you go keeps the first-seen order without storing any order, and each line costs one average constant-time lookup.
- Follow-ups
-
- Estimate the memory for 5 million distinct IDs.
- The distinct IDs no longer fit in memory.
- Two JSON records count as the same when their user and email fields match.
From the lesson Sets and frozensets for membership and maths
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 4 minutes
-
Fresher to mid
A unit test checks that str(active_users()) equals "{'asha', 'ravi', 'mei'}", where active_users() returns a set. It passes on a developer's laptop and fails in CI now and then. Explain the cause, why a similar test on a set of small integers never failed, and fix the test.
- What it tests
- Hash randomisation, what decides the order of a set, and testing results that have no order.
- First step
- Point at what the test compares: text. A set has no defined order, and the order it prints follows the hashes of its items. String hashes are salted with a random value in each new Python process, so the printed order can change from run to run, and only one order matches the expected text.
- Follow-ups
-
- Why did a set of small integers always print in the same order?
- The function now returns a list built from the set. What should the test compare?
- When is setting PYTHONHASHSEED reasonable?
From the lesson Sets and frozensets for membership and maths
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 3 minutes
-
Fresher to mid
A report computes total = sum([len(line) for line in log_file]) over a log of 50 million lines. On a small server it runs out of memory before printing anything. Explain why, fix it, and say when you would still build the list.
- What it tests
- The difference between a list comprehension and a generator expression in memory, what a list of integers really costs, and when building a list is the right call.
- First step
- The brackets build the whole list of 50 million lengths before sum() sees the first one: about 420 MB of references on a 64-bit build, plus a 28-byte int object for every length above 256. Drop the brackets: sum(len(line) for line in log_file) hands sum() one length at a time, so memory stays flat whatever the size of the file.
- Follow-ups
-
- How would you measure the memory each version needs?
- The report also needs the median line length.
- Is map(len, log_file) better than the generator expression?
From the lesson List, dict and set comprehensions
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 4 minutes
-
Mid to senior
This works at module level but raises NameError in a class body: class Grid: size = 8; cells = [(r, c) for r in range(size) for c in range(size)]. Which size fails, and why? Give two fixes, and say whether a generator expression or a lambda in the class body would behave differently.
- What it tests
- Class scopes and comprehension scopes, which part of a comprehension runs in the enclosing scope, and applying the same rule to generator expressions and lambdas.
- First step
- Split the comprehension into its parts. Only the iterable of the first for clause, the first range(size), is evaluated in the class body. The second range(size) runs inside the comprehension's own scope, and names defined in a class body are invisible to nested scopes, so that lookup goes on to the globals and fails.
- Follow-ups
-
- Why does Python not let nested scopes see class variables?
- Did the inlined comprehensions of Python 3.12 change this?
- How would you write cells so that it works and stays readable?
From the lesson List, dict and set comprehensions
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 5 minutes
-
Mid to senior
A monitor counts API calls in calls = defaultdict(int). A dashboard runs is_heavy(user), returning calls[user] > 1000, for all 3 million users in the directory, most of whom never call the API. Memory jumps by over 100 MB and len(calls), the active-user metric, becomes 3 million. Explain, fix it, and say how a test catches it.
- What it tests
- Knowing that a defaultdict read with square brackets inserts, the memory an entry costs, and testing side effects as well as return values.
- First step
- Reading calls[user] for a missing user runs the factory: int() returns 0, and the defaultdict stores that 0 under the user before returning it. Every check of a silent user therefore adds an entry. Check with calls.get(user, 0) > 1000 instead, or make calls a Counter, whose reads of missing keys return 0 without storing anything.
- Follow-ups
-
- How much memory does one extra entry cost?
- How would a test catch this?
- When is a defaultdict still the right type here?
From the lesson collections: deque, Counter, defaultdict and more
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 4 minutes
-
Fresher to mid
A service receives 50,000 sensor readings a second and, after each one, reports the average of the last 1,000. Compare a list with append() and pop(0), a deque(maxlen=1000) with sum() each time, and a deque with a running total. What does each cost per reading, and which would you ship?
- What it tests
- Counting the work per operation, seeing which cost dominates, and keeping an O(1) aggregate over a sliding window.
- First step
- Count the work per reading. The list version moves 999 references on every pop(0) and then adds 1,000 values. The bounded deque drops the oldest reading in constant time, but sum() still adds 1,000 values. A running total adds the new reading and subtracts the one that leaves, which is constant work however long the window is.
- Follow-ups
-
- What can go wrong with a running total of floats over months?
- The window is the last 60 seconds, not the last 1,000 readings.
- How would you also report the maximum of the window?
From the lesson collections: deque, Counter, defaultdict and more
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 4 minutes
-
Fresher to mid
A job scheduler pushes (priority, job) tuples onto a heapq heap. Tests pass, but production crashes with TypeError: '<' not supported between instances of 'Job' and 'Job'. Explain when it happens, fix it so that jobs of equal priority run in submission order, and let callers cancel a job that is already queued.
- What it tests
- How tuples compare item by item, a stable priority queue with a unique tie-breaker, and removing entries from a heap without breaking it.
- First step
- Tuples compare item by item, so whenever two priorities are equal, heapq compares the jobs themselves, and Job defines no ordering. The tests probably never had two pending jobs with the same priority meet in a comparison. Push (priority, next(sequence), job) with sequence = itertools.count(): the number is unique, so comparisons stop before the jobs, and ties keep submission order.
- Follow-ups
-
- Why not just define __lt__ on Job?
- How do you change the priority of a job that is already queued?
- Several threads submit jobs at the same time.
From the lesson heapq, bisect and array for efficient data
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 4 minutes
-
Mid to senior
A file holds 50 million transaction amounts, one per line. Report the 10 largest using as little memory as you can. Compare sorted(), heapq.nlargest(10, ...) and a min-heap of size 10 kept by hand, in time and memory, and say when you would choose each.
- What it tests
- Streaming with bounded memory, the size-k min-heap pattern, and comparing O(n log n) sorting with O(n log k) selection with real numbers.
- First step
- sorted() must hold all 50 million values, about 2 GB as Python floats and references, and does O(n log n) work. A min-heap of the 10 largest seen so far keeps its smallest at heap[0]: each new amount is compared with heap[0] and replaces it with heapreplace() only when larger. That is O(n log 10) time and O(10) memory, and heapq.nlargest(10, stream) does exactly this.
- Follow-ups
-
- What changes if you need the top 10 per merchant?
- The amounts are in 20 files on different machines.
- Why is a min-heap, not a max-heap, the right shape here?
From the lesson heapq, bisect and array for efficient data
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 4 minutes
-
Mid to senior
A script reads export.csv with open("export.csv", newline="") and csv.DictReader. Exports from your server work. A file a colleague saved from a spreadsheet program fails with KeyError: 'name', and on the colleague's Windows laptop the script also prints José as José. Explain both symptoms and fix the script.
- What it tests
- Recognising a UTF-8 byte order mark and mojibake from the locale encoding, reading text files portably, and proving an encoding from the bytes.
- First step
- Treat them as two decoding mistakes. The spreadsheet program wrote a byte order mark, ef bb bf, before the header, and decoding as plain UTF-8 keeps it as an invisible U+FEFF stuck to "name". On Windows, open() without encoding= uses the locale's code page, often cp1252, which turns the two UTF-8 bytes of é into é. Open the file with encoding="utf-8-sig".
- Follow-ups
-
- How would you prove which encoding the file really uses?
- Some customers send files that really are in Windows-1252.
- What changes for this script in Python 3.15?
From the lesson Text vs bytes: Unicode, encodings, bytearray
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 4 minutes
-
Mid to senior
Users sign up with names typed on many devices. Your duplicate check treats two sign-ups that both display as Zoë as different people, and two Devanagari names that look identical are also stored twice. Explain why, and design what you store, what you compare, and what you use for unique login names.
- What it tests
- Composed and decomposed forms, NFC against NFKC, caseless matching, and separating stored text, comparison keys and identifiers.
- First step
- Compare code points, not pixels: one Zoë has ë as the single code point U+00EB, the other an e followed by the combining diaeresis U+0308, so == sees different strings; the Devanagari pair differs the same way around a nukta. Normalise to NFC when text enters the system, store that, and compare NFC forms, adding casefold() where case must not matter.
- Follow-ups
-
- Why not normalise with NFKC everywhere?
- Two names use different scripts but look the same, such as a Latin a and a Cyrillic а.
- How do you clean up the duplicates already stored?
From the lesson Text vs bytes: Unicode, encodings, bytearray
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 4 minutes
-
Mid to senior
A game server keeps 2 million (score, player) pairs in a list and calls sorted() on every request to show the top 10 and the requesting player's rank. Scores change thousands of times a second. Redesign the in-memory structures and give the cost of each operation.
- What it tests
- Splitting operations by frequency, combining a dict with an ordered structure, and putting numbers on sort, insort and rank queries.
- First step
- List the operations with their rates: score updates, thousands a second; the top 10 and one player's rank, every page. Sorting 2 million pairs per request costs seconds of CPU for ten names and one number. Keep a dict from player to score for updates, and a separate structure for order, such as counts per score in a Fenwick tree, so that updates and ranks cost O(log n).
- Follow-ups
-
- What does insort cost with 2 million entries, and when does it stop being acceptable?
- Ranks only need to be approximate, say to the nearest 1 %.
- The leaderboard must survive a restart.
From the lesson Choosing the right data structure
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 4 minutes
-
Mid to senior
Design an in-process rate limiter: each user may make at most 100 requests in any 60-second window. Which structures hold the state, what does one request cost, and how do you stop memory from growing with users who have stopped calling?
- What it tests
- Choosing a dict of deques for sliding windows, amortised costs, memory per user, approximate alternatives and cleaning up idle state.
- First step
- Keep a dict from user to a deque of the timestamps of their recent allowed requests. On each request, pop timestamps older than 60 seconds from the left, then allow the request if fewer than 100 remain and append its timestamp. Every timestamp is appended once and popped once, so a request costs amortised O(1).
- Follow-ups
-
- How much memory does one active user cost?
- Can you use less memory if the limit may be approximate?
- Several processes serve the same users.
From the lesson Choosing the right data structure
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 4 minutes