A list is an ordered bag that you can change. A tuple is an ordered bag that you cannot change. A set keeps unique values and has no order. A dict maps keys to values.
Interview Q&A
๐ Python
Language basics, output puzzles, and data-engineer coding.
73 questions ยท 35 theory ยท 38 coding
Q-PY-001 List vs tuple vs set vs dict
Answer
Explanation
Think of a list as a column you keep editing. Think of a tuple as a fixed row shape, like (user_id, event_time). A set is for membership tests: "is this id already seen?" A dict is for lookup by name: row["country"]. In a pipeline, lists grow as you collect records, sets drop duplicates, and dicts hold config or a join map. Sets and dicts need hashable items or keys.
Trap
Saying a set is "a list without duplicates that keeps order." A set does not keep insertion order as a promised API for data work, and it cannot hold lists.
Q-PY-002 What are Python's mutable and immutable types?
Answer
Mutable objects can change in place. Lists, dicts, and sets are mutable. Immutable objects cannot change in place. Ints, floats, strings, tuples, and frozensets are immutable.
Explanation
x = [1] then x.append(2) edits the same list. s = "ab" then s += "c" makes a new string. That difference matters when two names point at one object. If a function appends to a list you passed in, the caller sees the change. If a function does n = n + 1, the caller's number does not change. Production bug: putting a list inside a set, or using a list as a dict key, fails because a list is not hashable.
Trap
Calling a tuple fully immutable in every case. A tuple cannot change its slots, but if a slot holds a list, that inner list can still change.
Q-PY-003 Difference between `is` and `==`?
Answer
== asks if two values look equal. is asks if two names point to the same object in memory. Use is for None. Use == for numbers, strings, lists, and dicts.
Explanation
[1, 2] == [1, 2] is True because the values match. [1, 2] is [1, 2] is False because they are two lists. x is None is the normal None check. CPython caches small integers, so 256 is 256 may be True while larger ints may not. Do not use that cache in production. In data jobs, compare file names and ids with ==, and compare to missing with is None.
Trap
Using is to compare strings or numbers because it "worked in the REPL." That can fail for longer strings and larger ints.
Q-PY-004 List comprehension vs generator expression
Answer
A list comprehension builds the whole list now. A generator expression yields one item at a time. Use a list when you need the full result. Use a generator when the source is large and you only walk it once.
Explanation
[x * 2 for x in nums] keeps every answer in memory. (x * 2 for x in nums) does not. You can loop the generator, but you cannot index it, and you cannot walk it twice. For a 10 GB log, a generator keeps memory flat. For a small lookup table you will reuse, a list is simpler. Pandas and Spark are different tools. This choice is about plain Python loops.
Trap
Writing a generator, then calling len() or using it twice. The second loop is empty because the generator is already spent.
Q-PY-005 What is the difference between deepcopy and shallow copy?
Answer
A shallow copy makes a new outer box, but nested objects are still shared. A deep copy copies the nested objects too. Use copy.copy for one level. Use copy.deepcopy when nested lists or dicts must not share memory.
Explanation
b = a.copy() on a list of lists still shares the inner lists. Change b[0][0] and a changes too. copy.deepcopy(a) breaks that link. Deep copy is slower and can fail on open files or Spark objects. In ETL, copy a nested JSON config before a task mutates it. For a flat list of ints, shallow copy is enough.
Trap
Believing b = a[:] always isolates nested data. That slice is a shallow copy.
Q-PY-006 What are `*args` and `**kwargs`?
Answer
*args collects extra positional arguments into a tuple. **kwargs collects extra named arguments into a dict. You also use * and ** to unpack a list or dict into a call.
Explanation
def load(path, *args, **kwargs) can take extra flags without listing every name. Helpers like pandas.read_csv(path, **opts) pass a config dict through. Order in a signature is: normal args, *args, keyword-only args, **kwargs. In production, prefer real names for required settings. Use kwargs for optional passthrough, then log the keys you accepted.
Trap
Writing **args or treating args as a dict. args is a tuple. kwargs is a dict.
Q-PY-007 What is a decorator?
Answer
A decorator is a function that wraps another function. The @name line above a def applies that wrapper. People use decorators for timing, retries, cache, and access checks.
Explanation
@timer above def run(): means run = timer(run). The wrapper can run code before and after the real function. functools.wraps keeps the original name and docstring. In a data job, a retry decorator can catch a flaky API. Keep wrappers thin. Heavy logic inside a decorator is hard to test and hard to see in a stack trace.
Trap
Forgetting to return the inner function, or forgetting to return the real result. Then the decorated function becomes None.
Q-PY-008 What is the GIL (Global Interpreter Lock)?
Answer
The GIL is a lock inside CPython. Only one thread runs Python bytecode at a time. Threads can still help when you wait on disk, network, or some C libraries. CPU-heavy Python loops do not get faster with more threads.
Explanation
CPython uses the GIL to keep memory management simple. During time.sleep or a network read, the lock can be released, so other threads run. A pure-Python JSON parse on a huge string will not use four cores with threading. Use multiprocessing, a native library, Spark, or a warehouse SQL job for CPU work. Pandas groupby is often still one process. Spark executors are processes, not Python threads.
Trap
Saying "Python cannot do two things at once." Processes, I/O waits, and C extensions can overlap work. The GIL is about bytecode in one CPython process.
Q-PY-009 What are Python generators?
Answer
A generator is a function that uses yield. Each yield pauses and gives back one value. The function keeps its place until the next value is asked for.
Explanation
def rows(): yield 1; yield 2 does not build [1, 2] first. next() or a for-loop pulls values. That is how you stream a large file line by line. Generators are iterators, so they run once. If you need the data twice, materialize a list or open the file again. In ETL, a generator pipeline can read, clean, and write without holding the full dataset.
Trap
Collecting every yield into a list "just in case." That throws away the memory benefit.
Q-PY-010 What is the difference between an iterable and an iterator?
Answer
An iterable can give you an iterator, usually with iter(x) or a for-loop. An iterator is the object that has next(). A list is iterable and can be looped many times. A file or generator is already an iterator and is spent after one pass.
Explanation
You can loop a list many times. You can loop a file or a generator only once. zip, map, and enumerate return iterators in Python 3. for x in data calls iter(data) for you. In production, do not pass a spent iterator into a second stage and wonder why output is empty. If a function should be reusable, take a path or a list, not a half-consumed file handle.
Trap
Calling iter() on an iterator and expecting a reset. iter(iterator) usually returns the same spent object.
Q-PY-011 How does `try/except/else/finally` work?
Answer
try is the risky code. except runs if that error type is raised. else runs if no error happened. finally always runs, even after return.
Explanation
Open a file in try, parse in the same block, and handle ValueError in except. Put "parse succeeded" logging in else, so you do not log success after a caught error. Close resources in finally, or better, use with. Catch specific errors. except Exception can hide bugs. Never use bare except:. In jobs, fail loudly on schema errors, and retry only on timeout or connection errors.
Trap
Putting the success path in except or catching Exception around a whole DAG task. You swallow the real bug and mark the job green.
Q-PY-012 What is a context manager?
Answer
A context manager is the object used in a with block. It sets something up, then it is guaranteed to clean up. Files, locks, and DB sessions are the common cases.
Explanation
with open(path) as f: opens the file and closes it even if the body raises. You can write your own with __enter__ and __exit__, or with contextlib.contextmanager. In production, always close sockets, temp files, and Spark sessions this way. __exit__ can see the exception. Return True only if you really handled it. Swallowing errors in __exit__ hides failures.
Trap
Opening a file, then returning the handle from a function without with at the caller. The file stays open until GC, which is late on a busy worker.
Q-PY-013 Multiprocessing vs threading
Answer
Threads share memory in one process, but the GIL limits CPU Python. Processes have separate memory and can use many cores. Threads are lighter for I/O. Processes are the tool for CPU-bound Python.
Explanation
A thread pool is good for many HTTP calls or many small file reads. A process pool is good for heavy JSON, regex, or pandas on chunks. Process data must be pickled to the child, so huge objects are expensive to send. Shared memory and queues exist, but they add complexity. Data engineers usually scale with Spark, warehouse compute, or many container workers, not with 32 Python threads in one task.
Trap
Starting a Process pool for tiny functions on tiny lists. Startup and pickle cost can be slower than a simple loop.
Q-PY-014 What is asyncio in simple terms?
Answer
Asyncio is cooperative concurrency on one thread. A task yields while it waits on I/O, and another task runs. It is not a way to use many CPU cores.
Explanation
async def functions are coroutines. await pauses until a network or timer finishes. Thousands of API calls can share one event loop. CPU work still blocks the loop, so do not parse a giant file inside a coroutine without an executor. Most batch ETL does not need asyncio. Use it for many slow HTTP lookups. Mixing blocking requests with asyncio is a common production stall.
Trap
Putting time.sleep or heavy pandas inside async def and expecting overlap. That blocks the whole loop.
Q-PY-015 When would you use `__slots__`?
Answer
__slots__ lists allowed attributes and skips the per-instance __dict__. That saves memory when you create millions of small objects. You cannot add random attributes later.
Explanation
A normal instance stores fields in a dict. That is flexible and a bit heavy. __slots__ = ("id", "ts") stores those fields more tightly. dataclasses.dataclass(slots=True) does the same with less boilerplate. Use this for huge in-memory event objects. Do not use it for everyday config classes. Multiple inheritance with slots is tricky. Pandas and Spark already store columns densely, so slots rarely beat a DataFrame.
Trap
Adding __slots__ and then setting self.extra = 1. That raises AttributeError.
Q-PY-016 What is a dataclass?
Answer
A dataclass is a class that auto-builds __init__, __repr__, and __eq__ from typed fields. It is a clean way to hold records or config. frozen=True makes the instance read-only.
Explanation
@dataclass class Job: name: str; retries: int = 3 gives you Job("load", 5). Frozen dataclasses can be hashable if every field is hashable, so they can be dict keys. Prefer dataclasses over a raw dict when the shape is known. Prefer them over namedtuple when you want defaults and methods. In pipelines, keep them for config and small rows, not for million-row tables.
Trap
Mutating a default list field on a dataclass the same way as a mutable function default. Use field(default_factory=list).
Q-PY-017 How does Python typing help in interviews and jobs?
Answer
Type hints tell readers what a function takes and returns. They are not enforced at runtime unless you add a checker or a validator. Use list[str], dict[str, int], and str | None for missing values.
Explanation
def get_country(row: dict[str, str]) -> str | None documents a lookup that can miss. Tools like mypy or pyright catch mistakes before a job runs. Runtime is still dynamic, so a bad CSV can still pass a hint. In production, pair hints with real validation for external data. Keep hints honest. list[Any] hides the problem you were trying to show.
Trap
Saying type hints make Python statically typed like Java. They do not, unless you run a separate checker.
Q-PY-018 Why is a mutable default argument dangerous?
Answer
Default values are created once, when the function is defined. A default list or dict is then shared across calls. Later calls see leftover data. Use None and create a new list inside the function.
Explanation
def add(item, bucket=[]): appends into the same list forever. add(1) then add(2) returns [1, 2] on the second call. The fix is bucket=None and if bucket is None: bucket = []. This bites helper functions that collect errors or rows. In production it looks like "state leaked between DAG runs" even in one process.
Trap
Blaming the caller for "passing the same list." The caller passed nothing. The default object was reused.
Q-PY-019 How should you read files in Python?
Answer
Use with open(path, encoding="utf-8") as f:. Read line by line for large files. Use pathlib.Path for paths. Close is automatic when the with block ends.
Explanation
f.read() loads the whole file. That is fine for small JSON. For logs, for line in f: streams. Always set encoding. Default encoding is not the same on every OS. Binary files need "rb". In jobs, prefer a context manager so a parse error still closes the handle. For cloud storage, use the vendor client, then the same line-by-line idea.
Trap
Hard-coding open(path) without encoding, then failing on Windows or on a UTF-8 BOM.
Q-PY-020 How does JSON work in Python?
Answer
json.loads reads a string. json.dumps writes a string. json.load and json.dump work on files. JSON has objects, arrays, strings, numbers, booleans, and null, but not datetime, set, or tuple.
Explanation
A dict becomes a JSON object. A list becomes an array. None becomes null. Tuples dump as arrays, so they come back as lists. Dates need default=str or an encoder, and you must parse them back. dumps is not the same as dump. In pipelines, pretty indent=2 is for humans. Compact JSON is smaller for storage. Watch trailing commas. Standard JSON does not allow them.
Trap
Using eval on JSON, or expecting json.loads to rebuild a datetime or a tuple.
Q-PY-021 How should you read CSV in Python?
Answer
Use the csv module or pandas. csv.DictReader gives one dict per row. Do not split a line on commas yourself. Quotes and commas inside fields will break that.
Explanation
CSV looks simple and is not. Encoding, delimiter, header names, and quoting all matter. DictReader maps the header row to values. Empty fields come through as empty strings, not None. In production, check the delimiter. Europe often uses ;. For huge files, stream rows. For analytics, pandas or Spark is easier, but you should still know the stdlib for small jobs and unit tests.
Trap
line.split(",") on a quoted CSV. "Acme, Inc",42 becomes three fields instead of two.
Q-PY-022 What is `Counter`?
Answer
Counter is a dict that counts hashable items. Counter(words)["error"] is the count. most_common(n) gives the top n items.
Explanation
from collections import Counter then Counter(["a", "a", "b"]) is {"a": 2, "b": 1}. Missing keys return 0, not KeyError. You can add counters. This is the fast answer for word count, duplicate ids, and status-code histograms. For a 20 GB file, count in a loop or in Spark. Do not load every token into a giant list first if you can stream.
Trap
Using dict[key] += 1 without defaultdict(int) or Counter, then hitting KeyError on the first see.
Q-PY-023 What is `heapq` used for?
Answer
heapq is a min-heap on a Python list. It gives you the smallest item fast. Use it for top-k, "next event" scheduling, and merging sorted streams.
Explanation
heappush and heappop keep the smallest at index 0. For the k largest, Python has nlargest. For the k smallest, nsmallest. You can store tuples like (priority, item). For top 10 amounts in 10 million rows, a heap of size 10 beats sorting the whole list. Pandas nlargest does the same idea on a column. heapq is the stdlib version.
Trap
Treating the whole heap list as sorted. Only the first item is guaranteed to be the smallest.
Q-PY-024 What is `itertools` good for in data jobs?
Answer
itertools builds memory-light iterators. Common tools are groupby, chain, islice, product, and combinations. They shine when you stream data and do not want extra lists.
Explanation
chain(a, b) walks two iterables as one. islice(rows, 100) takes a head without slicing a giant list. groupby groups consecutive keys only, so sort first if you need all matching keys together. product can explode into a huge cartesian set, so be careful. In production, groupby on already-sorted log partitions is cheap. A dict of lists is simpler if the data is small and unsorted.
Trap
Using itertools.groupby on unsorted data and thinking all equal keys are in one group.
Q-PY-025 What is the walrus operator `:=`?
Answer
:= assigns a value inside an expression. People use it to compute once and also test the result. It needs Python 3.8 or later.
Explanation
while (line := f.readline()) != "": reads and checks in one place. if (m := pat.search(text)): saves the match without a second search. That avoids double work. Keep it readable. Nested walrus in a huge comprehension is hard to debug. In interviews, show one clean use. In production, prefer a plain assignment if the line is already busy.
Trap
Writing == by mistake, or using walrus where a normal variable would be clearer.
Q-PY-026 What is `match/case`?
Answer
match/case is structural pattern matching, added in Python 3.10. It can branch on values, unpack sequences, and pull keys from dicts. case _ is the default.
Explanation
match status: case 200: ... case 404: ... case _: ... is a clean switch. It can also do case {"level": "ERROR", "job": job}:. That is useful for event payloads. Patterns can bind names. A subject that matches a pattern does not need a chain of if/elif. In older runtimes, this syntax is a SyntaxError, so check the platform version before you ship it.
Trap
Treating it like a C switch that only compares equals. A case like case [x, y]: unpacks a two-item sequence, it does not only test equality.
Q-PY-027 How do f-strings work?
Answer
An f-string puts expressions inside {} in a string. f"rows={n}" fills in n. You can format numbers, like f"{x:.2f}".
Explanation
name = "sales"; f"table={name}" becomes "table=sales". {x=} is a debug form that prints the name and value. F-strings are usually faster and clearer than % or .format(). Do not build SQL or shell commands by sticking raw user text into an f-string. That is injection. Use parameters for SQL and shlex or argument lists for commands.
Trap
Putting a backslash in the expression part, or using f-strings as a safe SQL builder.
Q-PY-028 What does `*` unpacking do?
Answer
* unpacks a sequence into positional pieces. ** unpacks a dict into named pieces. You can also catch leftovers: first, *rest = row.
Explanation
f(*args) is like listing each item. f(**{"a": 1}) is like f(a=1). merged = {**a, **b} copies keys, with b winning on clashes. In Python 3.9+, a | b also merges dicts. Star unpacking in a call must match the function signature. In data code, head, *tail = parts is a clean way to split a log line after you already split it.
Trap
Unpacking the wrong length, like a, b = [1, 2, 3], which raises ValueError.
Q-PY-029 How does sorting work with `key` and stability?
Answer
sorted(x) returns a new list. x.sort() changes a list in place. key= is a function that picks the sort value. Python sort is stable, so equal keys keep their old order.
Explanation
sorted(rows, key=lambda r: r["amount"], reverse=True) sorts high amount first. For more than one column, return a tuple: key=lambda r: (r["country"], -r["amount"]). Stability means you can sort by amount, then by country, in two passes if you want. sort only works on lists. Use sorted for tuples, dict keys, or any iterable. In production, say how ties are broken.
Trap
Sorting a list of dicts without a key, or comparing None with numbers and getting a TypeError.
Q-PY-030 How do you group records in plain Python?
Answer
The usual pattern is a dict of lists or a defaultdict(list). For each row, append it under its key. itertools.groupby only groups consecutive equal keys, so sort first if you use it.
Explanation
To sum amount by country, do totals[country] += amount. That is the pandas groupby idea without pandas. Keep the grain clear: one output row per group key. If you need all member rows, store lists. If you only need a sum, store a number. For huge data, this dict must fit in memory, or you move the group to Spark or SQL.
Trap
Using itertools.groupby without sorting, then missing rows that belong to the same key but were not next to each other.
Q-PY-031 How does truthiness work with `None`, `0`, and empty collections?
Answer
None, 0, 0.0, "", [], {}, and set() are falsy. A non-empty list or a non-zero number is truthy. Use is None when you mean missing, not when you mean empty.
Explanation
if rows: is true when there is at least one row. if rows is None: is true only for missing. Those are different. any([]) is False and all([]) is True, because there is no counterexample. In ETL, a missing column and an empty string are not the same. Do not write if not value when 0 is a valid amount.
Trap
Treating 0 as missing, or treating [] the same as None in an API response.
Q-PY-032 Why must dict keys and set items be hashable?
Answer
Dicts and sets use a hash table. A key must have a stable hash and a working equality check. Lists, dicts, and sets are mutable, so they are not hashable. Strings, numbers, and frozensets are.
Explanation
{"a": 1} is fine. {[1, 2]: "x"} raises TypeError. A tuple is hashable only if every item inside is hashable. (1, 2) can be a key. ([1], 2) cannot. This is why join maps use ids and names, not whole nested records, unless you freeze them. In production, pick a natural key that will not change, like an id, not a mutable row dict.
Trap
Trying to put a dict inside a set to "deduplicate records." Convert to a tuple of items, or use a dict keyed by id.
Q-PY-033 Difference between `append()` and `extend()`?
Answer
append(x) adds x as one item at the end. extend(x) walks x and adds each item. append([1, 2]) makes a nested list. extend([1, 2]) adds 1 and 2.
Explanation
If you are collecting rows, rows.append(row) is correct. If you are concatenating two lists, rows.extend(more) or rows += more is correct. append of a list is a common accidental nest. That later breaks a flatten or a DataFrame constructor. + on lists makes a new list. extend edits in place.
Trap
Using append in a loop on another list and then wondering why you have lists inside lists.
Q-PY-034 What is a lambda function?
Answer
A lambda is a tiny unnamed function. It can only be one expression. People use it as a key for sort, or as a short map.
Explanation
sorted(rows, key=lambda r: r["ts"]) sorts by timestamp. lambda x: x * 2 is the same idea as def double(x): return x * 2. If the body needs if/else blocks or a name in a stack trace, write a real def. In production, named functions are easier to test. Interviewers still expect you to read a lambda on a key=.
Trap
Trying to put statements inside a lambda, or using a lambda when a generator expression is clearer.
Q-PY-035 What does `if __name__ == "__main__":` do?
Answer
A module's __name__ is "__main__" only when you run that file directly. The if block holds script code. Importing the file does not run that block.
Explanation
python job.py sets __name__ to "__main__" and runs the loader. import job from a test does not start the load. That split lets you reuse functions. In production, keep side effects like "read a file and write output" under this guard, or under a real CLI. Spark and Airflow should import functions, not import a module that starts a job on contact.
Trap
Putting heavy work at module top level. Tests, Spark workers, and Flask reloads may import the file and run the job twice.
Q-PY-036 Reverse the words in a sentence
Answer
Split the sentence on whitespace. Reverse the list of words. Join them with one space. This changes word order, not letter order.
Explanation
"data engineer interview" becomes "interview engineer data". split() with no argument collapses extra spaces, which is what you want for messy logs. If you reverse the whole string with [::-1], letters flip too. In production, decide if punctuation stays attached to a word.
Code
def reverse_words(text):
# split() with no argument cuts on any whitespace and drops extras
words = text.split()
# reverse the word list in place
words.reverse()
# put one space back between the words
return " ".join(words)
# sample sentence
sample = "data engineer interview"
# print the reversed words
print(reverse_words(sample))
# Output: interview engineer data
What this code does
- First we split the text into a list of words.
- Then we reverse that list.
- Then we join the words with a single space and print the result.
Trap
Using text[::-1]. That reverses letters, so "abc def" becomes "fed cba".
Q-PY-037 Check if two strings are anagrams
Answer
Two strings are anagrams if they use the same characters with the same counts. Ignore case and spaces if the prompt says so. Counter is the clean check.
Explanation
"listen" and "silent" match. "python" and "typhon" match. "python" and "typhoon" do not. Sorting both strings also works, but counting is linear. In data quality, this is the same idea as "same multiset of tokens."
Code
from collections import Counter
def is_anagram(left, right):
# normalize case and drop spaces so "Listen" matches "silent"
a = left.lower().replace(" ", "")
b = right.lower().replace(" ", "")
# Counter builds a {char: count} map for each string
return Counter(a) == Counter(b)
# true pair
print(is_anagram("listen", "silent"))
# Output: True
# false pair
print(is_anagram("python", "typhoon"))
# Output: False
What this code does
- First we lower-case both strings and remove spaces.
- Then we count characters in each string.
- Then we compare the two count maps.
Trap
Sorting after lower-case but forgetting spaces. "a b" and "ab" then look different or the same depending on the missed step.
Q-PY-038 Two sum
Answer
Walk the list once. Store each number and its index in a dict. For value x, look up target - x. If it is already in the dict, you have the pair.
Explanation
This is O(n) time and O(n) extra space. Nested loops are O(n^2) and fail interviews on large lists. Ask if values can repeat and if you may use the same index twice. The usual rule is two different indexes.
Code
def two_sum(nums, target):
# seen maps a value we already walked to its index
seen = {}
# i is the current index, n is the current value
for i, n in enumerate(nums):
# this is the partner we need
need = target - n
# if the partner was seen earlier, return both indexes
if need in seen:
return [seen[need], i]
# remember this value for a later partner
seen[n] = i
# no pair found
return None
# 2 + 7 = 9
print(two_sum([2, 7, 11, 15], 9))
# Output: [0, 1]
What this code does
- First we make an empty dict of seen values.
- Then we compute the partner each number would need.
- Then we return the two indexes when that partner is already in the dict.
Trap
Using the same index twice, like matching 4 with itself for target 8 when 4 appears only once.
Q-PY-039 Flatten a nested list
Answer
Walk each item. If it is a list, flatten it. If it is not a list, keep it. A stack is safer than deep recursion on huge nests.
Explanation
[1, [2, [3, 4], 5]] becomes [1, 2, 3, 4, 5]. Ask what "nested" means. Tuples? Dicts? Only lists? In ETL this shows up when JSON arrays sit inside arrays. Recursion depth in Python is limited, so a stack is safer for wild payloads.
Code
def flatten(items):
# out holds the flat values in order
out = []
# stack holds pieces still to walk, rightmost item is next
stack = list(reversed(items))
while stack:
# take the next piece
item = stack.pop()
# if it is a list, put its children on the stack in reverse
if isinstance(item, list):
stack.extend(reversed(item))
else:
# a real value, keep it
out.append(item)
return out
print(flatten([1, [2, [3, 4], 5], 6]))
# Output: [1, 2, 3, 4, 5, 6]
What this code does
- First we put the top-level items on a stack.
- Then we pop one item at a time.
- Then we either expand a nested list or append a real value.
Trap
Using str as the base case and flattening "ab" into "a" and "b". Strings are sequences, so check isinstance(item, list) not "is iterable."
Q-PY-040 Word count
Answer
Lower-case the text. Split into words. Count with Counter. Return the mapping, or the top word if that is what they asked.
Explanation
This is the classic MapReduce example in one process. For a file, stream lines and update one Counter. Do not build one giant string if the file is huge. Decide what a word is: split on whitespace, or strip punctuation.
Code
from collections import Counter
def word_count(text):
# lower-case so "Error" and "error" are the same token
lowered = text.lower()
# split on whitespace
words = lowered.split()
# count each word
return Counter(words)
sample = "error timeout error retry timeout error"
counts = word_count(sample)
# print the full counts
print(dict(counts))
# Output: {'error': 3, 'timeout': 2, 'retry': 1}
# print the most common word
print(counts.most_common(1))
# Output: [('error', 3)]
What this code does
- First we lower-case the text.
- Then we split it into words.
- Then we count the words and print totals plus the top word.
Trap
Splitting only on a single space and leaving punctuation stuck to words, so "error." is not "error".
Q-PY-041 Top-N items from a dict
Answer
Sort the dict items by value, descending. Take the first N. heapq.nlargest also works and is better when N is small and the dict is huge.
Explanation
A status-code counter or a country-amount map often needs "top 3." Say how ties break. sorted is stable, but equal counts keep original order only if you sort the pairs that way. For a million keys and N=5, use a heap.
Code
import heapq
def top_n(counts, n):
# nlargest keeps the n pairs with the biggest count
# the key function reads the count, which is item[1]
return heapq.nlargest(n, counts.items(), key=lambda item: item[1])
data = {"US": 9, "IN": 12, "BR": 4, "DE": 12, "CA": 7}
print(top_n(data, 3))
# Output: [('IN', 12), ('DE', 12), ('US', 9)]
What this code does
- First we walk the dict as
(key, count)pairs. - Then we ask the heap for the three largest counts.
- Then we print those pairs.
Trap
Sorting by the key name instead of the value, or using max() in a loop and removing items, which is slower and messier.
Q-PY-042 Merge two dicts
Answer
Later keys win on a clash. {**a, **b} or a | b copies into a new dict. a.update(b) edits a in place.
Explanation
Config merge is a daily data-eng task: defaults, then job overrides. Ask if nested dicts should merge recursively. The | operator does one level only. Nested JSON usually needs a recursive merge or a library.
Code
def merge_dicts(base, override):
# copy base so we do not edit the caller's dict
out = dict(base)
# override keys replace base keys
out.update(override)
return out
defaults = {"retries": 3, "region": "us", "mode": "batch"}
job = {"region": "eu", "mode": "stream"}
print(merge_dicts(defaults, job))
# Output: {'retries': 3, 'region': 'eu', 'mode': 'stream'}
What this code does
- First we copy the base dict.
- Then we write override keys on top.
- Then we print the merged config.
Trap
Writing base.update(override) and also returning base, which mutates the original defaults for the next run.
Q-PY-043 Parse log lines into fields
Answer
Pick a regex with named groups, or split on a known delimiter. Skip lines that do not match. Keep the raw line for rows you cannot parse.
Explanation
Interviews love a line like 2024-01-02 10:11:12 ERROR job=load msg=timeout. Named groups are clearer than index numbers. In production, log formats drift. Count parse failures. Do not crash the whole job on one bad line unless the contract says the file is strict.
Code
import re
# named groups: ts, level, then the rest of the message
LOG = re.compile(
r"(?P<ts>\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}) (?P<level>[A-Z]+) (?P<msg>.*)"
)
def parse_logs(lines):
rows = []
for line in lines:
# try to match one log line
m = LOG.match(line)
if not m:
# skip junk, but in a real job you would also count it
continue
# groupdict() turns the names into a normal dict
rows.append(m.groupdict())
return rows
raw = [
"2024-01-02 10:11:12 ERROR job=load msg=timeout",
"not a log line",
"2024-01-02 10:11:13 INFO job=load msg=ok",
]
print(parse_logs(raw))
# Output: [{'ts': '2024-01-02 10:11:12', 'level': 'ERROR', 'msg': 'job=load msg=timeout'}, {'ts': '2024-01-02 10:11:13', 'level': 'INFO', 'msg': 'job=load msg=ok'}]
What this code does
- First we compile a regex with timestamp, level, and message groups.
- Then we match each line.
- Then we keep the parsed dicts and skip lines that do not match.
Trap
Using split(" ") on the whole line. A message with spaces will shift field positions.
Q-PY-044 Sliding window sum on a list
Answer
Keep a running sum of the last k items. Add the new item and drop the item that left the window. That is O(n), not O(n*k).
Explanation
Windows show up in moving averages, "errors in last 5 minutes," and time-series features. If the window is time-based, not count-based, you need a deque of timestamps. Here the window is a fixed length.
Code
def window_sums(nums, k):
# empty result if the window cannot even start
if k <= 0 or k > len(nums):
return []
out = []
# first window is a plain slice sum
running = sum(nums[:k])
out.append(running)
# i is the index of the new right edge
for i in range(k, len(nums)):
# add the new value
running += nums[i]
# drop the value that just left the window
running -= nums[i - k]
out.append(running)
return out
print(window_sums([1, 2, 3, 4, 5], 3))
# Output: [6, 9, 12]
What this code does
- First we sum the first k numbers.
- Then we slide one step: add the new right value and subtract the old left value.
- Then we collect each window sum.
Trap
Re-summing nums[i:i+k] every time. That is correct but slow, and interviewers want the running-sum trick.
Q-PY-045 Group records and sum without pandas
Answer
Use a dict keyed by the group column. Add the measure into that key. That is groupby().sum() in plain Python.
Explanation
Say the grain: one output row per country, or per (country, day). defaultdict(int) avoids KeyError. For more than one stat, store a small dict or a dataclass per key. If the data cannot fit in memory, do this in SQL or Spark instead.
Code
from collections import defaultdict
def group_sum(rows, key, value):
# totals[key] starts at 0 on first use
totals = defaultdict(int)
for row in rows:
# add this row's measure onto its group
totals[row[key]] += row[value]
# convert to a normal dict for a clean print
return dict(totals)
rows = [
{"country": "US", "amount": 10},
{"country": "IN", "amount": 7},
{"country": "US", "amount": 5},
{"country": "IN", "amount": 3},
]
print(group_sum(rows, "country", "amount"))
# Output: {'US': 15, 'IN': 10}
What this code does
- First we make a dict that defaults missing keys to 0.
- Then we add each row's amount into its country.
- Then we print one total per country.
Trap
Overwriting totals[country] = amount instead of adding. You keep only the last row in the group.
Q-PY-046 Write a decorator that times a function
Answer
Wrap the function. Record perf_counter before and after the real call, then print the elapsed time. Return the original result. Use functools.wraps so the name stays the same.
Explanation
This is a favorite live-coding prompt. time.time() can jump if the clock changes. perf_counter is for durations. In production, send the metric to logs or Prometheus, and do not print in tight loops.
Code
import time
from functools import wraps
def timed(fn):
# copy the original name and docstring onto the wrapper
@wraps(fn)
def wrapper(*args, **kwargs):
# start a monotonic clock
start = time.perf_counter()
# run the real function with the same arguments
result = fn(*args, **kwargs)
# measure how long it took
elapsed = time.perf_counter() - start
print(f"{fn.__name__} took {elapsed:.4f}s")
# give the original answer back
return result
return wrapper
@timed
def add(a, b):
# a tiny function so the decorator is easy to see
return a + b
print(add(2, 3))
# Output includes a timing line, then 5
What this code does
- First we wrap the function with
@timed. - Then we start a clock, call the real function, and stop the clock.
- Then we print the duration and return the original result.
Trap
Forgetting to return result, or forgetting to return wrapper from timed. Then the decorated function is None.
Q-PY-047 Build a generator pipeline
Answer
Each stage is a generator that yields to the next stage. Nothing runs until you loop the last stage. That keeps memory flat on large streams.
Explanation
Read lines, drop blanks, parse, then filter errors. Each function is small and testable. If you need to walk the data twice, this pattern is the wrong tool. Materialize or re-read.
Code
def lines(text):
# yield one raw line at a time from an in-memory file
for line in text.splitlines():
yield line
def not_blank(stream):
# drop empty or whitespace-only lines
for line in stream:
if line.strip():
yield line
def parse_level(stream):
# split "LEVEL message" into a pair
for line in stream:
level, _, msg = line.partition(" ")
yield level, msg
def only_error(stream):
# keep ERROR rows
for level, msg in stream:
if level == "ERROR":
yield msg
raw = "INFO start\n\nERROR timeout\nINFO done\nERROR disk"
# chain the stages; nothing runs until list()
print(list(only_error(parse_level(not_blank(lines(raw))))))
# Output: ['timeout', 'disk']
What this code does
- First we yield lines from the text.
- Then we drop blanks, split out the level, and keep ERROR rows.
- Then we materialize the last generator with
listso we can print it.
Trap
Calling list() in the middle of the pipeline. That loads everything and kills the streaming benefit.
Q-PY-048 Read a file line by line
Answer
Open with with and a UTF-8 encoding. Loop the file object. Strip the newline. For tests, io.StringIO behaves like a file.
Explanation
read() is fine for small files. Line iteration is the production habit for logs and CSV. Always set encoding. Close happens when the with block ends, even on error.
Code
from io import StringIO
def read_names(handle):
names = []
# a file object is already an iterator of lines
for line in handle:
# strip spaces and the trailing newline
name = line.strip()
if name:
names.append(name)
return names
# StringIO stands in for an open text file
fake_file = StringIO("alice\n\nbob\ncarol\n")
print(read_names(fake_file))
# Output: ['alice', 'bob', 'carol']
What this code does
- First we loop each line from the file-like object.
- Then we strip whitespace and skip empty lines.
- Then we return the cleaned names.
Trap
Loading handle.read().split("\n") for a huge file, or forgetting strip() and keeping "bob\n".
Q-PY-049 Remove duplicates and preserve order
Answer
Walk the list. Keep a seen set. Append an item only the first time it appears. Dict insertion order also works: list(dict.fromkeys(items)).
Explanation
set(items) drops duplicates but does not promise the original order as a data API you should rely on. The seen set is O(1) membership. Items must be hashable. For dict rows, dedup on an id field, not on the whole dict.
Code
def dedup_keep_order(items):
seen = set()
out = []
for item in items:
# skip if we already kept this value
if item in seen:
continue
# remember it, then keep it
seen.add(item)
out.append(item)
return out
print(dedup_keep_order(["b", "a", "b", "c", "a"]))
# Output: ['b', 'a', 'c']
print(list(dict.fromkeys(["b", "a", "b", "c", "a"])))
# Output: ['b', 'a', 'c']
What this code does
- First we make an empty set of values we have already kept.
- Then we append an item only when it is new.
- Then we print the order-preserving unique list.
Trap
Returning list(set(items)). Duplicates are gone, but the order may not match the source.
Q-PY-050 Find the second largest number
Answer
Track the largest and the second largest in one pass. Decide if duplicates count. The usual interview wants the second distinct value.
Explanation
Sorting and picking index -2 is easy and O(n log n). One pass is O(n). If every value is the same, there is no second largest. Say that case out loud.
Code
def second_largest(nums):
best = None
second = None
for n in nums:
# new unique maximum: old max becomes second
if best is None or n > best:
second = best
best = n
# n is between second and best, and not a duplicate of best
elif n != best and (second is None or n > second):
second = n
return second
print(second_largest([10, 5, 10, 8, 8]))
# Output: 8
print(second_largest([3, 3, 3]))
# Output: None
What this code does
- First we walk each number while holding the top and second-top unique values.
- Then we promote the old maximum when we see a bigger number.
- Then we return the second value, or None if it does not exist.
Trap
Sorting and returning sorted(set(nums))[-2] without checking length. A one-value list raises IndexError.
Q-PY-051 Check if a string is a palindrome
Answer
A palindrome reads the same forwards and backwards. Compare the string to its reverse. If the prompt says so, ignore case and non-letters.
Explanation
"abba" is a palindrome. "abca" is not. Two pointers from each end also work and use no extra copy. For interview speed, s == s[::-1] is enough after you clean the string.
Code
def is_palindrome(text):
# keep letters and digits, then lower-case
cleaned = "".join(ch.lower() for ch in text if ch.isalnum())
# compare with the reversed cleaned string
return cleaned == cleaned[::-1]
print(is_palindrome("Never odd or even"))
# Output: True
print(is_palindrome("data"))
# Output: False
What this code does
- First we drop spaces and punctuation and lower-case the rest.
- Then we reverse that cleaned string.
- Then we check equality.
Trap
Comparing the raw sentence. Spaces and capitals make "Never odd or even" look like a failure.
Q-PY-052 Fibonacci generator
Answer
Yield a, then walk a, b = b, a + b. Stop after n values, or yield forever and let the caller islice.
Explanation
A generator does not build the whole sequence first. That is the point. Do not use recursion with no cache in an interview unless they ask. It is exponential and hits the call stack.
Code
def fibonacci(n):
# first two Fibonacci numbers
a, b = 0, 1
# yield n numbers
for _ in range(n):
yield a
# move the window one step forward
a, b = b, a + b
print(list(fibonacci(8)))
# Output: [0, 1, 1, 2, 3, 5, 8, 13]
What this code does
- First we start with 0 and 1.
- Then we yield the left number n times.
- Then we shift the pair so the next left number is ready.
Trap
Writing a = b; b = a + b on two lines. The second line uses the new a, so the sequence is wrong.
Q-PY-053 Chunk a list into groups of size n
Answer
Slice items[i:i+n] while i jumps by n. The last chunk can be shorter. This is how you batch inserts or API calls.
Explanation
Empty input should return no chunks. n <= 0 should be rejected. In production, a generator of chunks is nicer than a list of lists if the source is huge.
Code
def chunk(items, n):
if n <= 0:
raise ValueError("n must be > 0")
out = []
# start at 0, n, 2n, ...
for i in range(0, len(items), n):
# slice does not fail if the tail is short
out.append(items[i:i + n])
return out
print(chunk([1, 2, 3, 4, 5], 2))
# Output: [[1, 2], [3, 4], [5]]
What this code does
- First we reject a non-positive chunk size.
- Then we walk start indexes 0, 2, 4, ...
- Then we slice each block, including a short last block.
Trap
Using items[i:i+n] but stepping i += 1, which makes overlapping windows, not batches.
Q-PY-054 Flatten nested JSON keys
Answer
Walk dicts and lists. Build a path string like user.address.city or items[0].id. Leaf values become the dict values. This is JSON normalize in a few lines.
Explanation
Nested payloads from APIs do not load cleanly into a table. Flattening makes one row with dotted columns. Lists need an index in the path. In production, exploding lists into child tables can be cleaner than one ultra-wide row.
Code
def flatten_json(obj, prefix=""):
out = {}
if isinstance(obj, dict):
for key, value in obj.items():
# join parent path with this key
path = f"{prefix}.{key}" if prefix else str(key)
out.update(flatten_json(value, path))
elif isinstance(obj, list):
for i, value in enumerate(obj):
# lists use [index] in the path
path = f"{prefix}[{i}]"
out.update(flatten_json(value, path))
else:
# a leaf value: store it
out[prefix] = obj
return out
payload = {"user": {"id": 7, "name": "Ada"}, "tags": ["etl", "python"]}
print(flatten_json(payload))
# Output: {'user.id': 7, 'user.name': 'Ada', 'tags[0]': 'etl', 'tags[1]': 'python'}
What this code does
- First we look at the current object type.
- Then we recurse into dict keys and list indexes while building a path.
- Then we store each leaf under that path.
Trap
Only flattening dicts and dropping lists, or using the same key tags for every list item so values overwrite.
Q-PY-055 Output puzzle โ mutable default argument
Answer
The default list is created once, when the function is defined. Later calls reuse that same list. That is why values pile up.
Explanation
This is the most famous Python output question. The fix is target=None and a new list inside the function. Interviewers want the reason, not only the printout.
Code
def add_item(item, target=[]):
# this default list is the SAME object on every call
target.append(item)
return target
print(add_item(1))
# Output: [1]
print(add_item(2))
# Output: [1, 2]
print(add_item(3))
# Output: [1, 2, 3]
def add_item_safe(item, target=None):
# make a fresh list when the caller did not pass one
if target is None:
target = []
target.append(item)
return target
print(add_item_safe(1))
# Output: [1]
print(add_item_safe(2))
# Output: [2]
What this code does
- First we append into a default list that lives on the function object.
- Then we call it three times and see the list grow.
- Then we show the
Nonesentinel fix, which makes a new list per call.
Trap
Saying Python "caches return values." It is the default object that is shared, not a cache of results.
Q-PY-056 Output puzzle โ nested list multiplication
Answer
[[]] * 3 copies the same inner list three times. Change one cell and every row changes. Build rows with a comprehension so each row is new.
Explanation
[0] * 3 is fine because ints are immutable. [[]] * 3 is not fine because lists are mutable. This shows up when people build a matrix.
Code
# three names, one inner list
bad = [[]] * 3
# append into "row 0"
bad[0].append("x")
print(bad)
# Output: [['x'], ['x'], ['x']]
# a new list for every row
good = [[] for _ in range(3)]
good[0].append("x")
print(good)
# Output: [['x'], [], []]
What this code does
- First we build a matrix with
[[]] * 3, which repeats one inner list. - Then we append to the first row and see all rows change.
- Then we rebuild with a comprehension so each row is a new list.
Trap
Thinking copy.copy(bad) will fix it. That is still a shallow copy of the outer list.
Q-PY-057 FizzBuzz
Answer
Explanation
This tests if/elif order, not math skill. In data terms it is a simple case mapping. A later match/case version is fine on Python 3.10+.
Code
def fizzbuzz(n):
out = []
for i in range(1, n + 1):
# 15 must be first because it is divisible by both 3 and 5
if i % 15 == 0:
out.append("FizzBuzz")
elif i % 3 == 0:
out.append("Fizz")
elif i % 5 == 0:
out.append("Buzz")
else:
out.append(str(i))
return out
print(fizzbuzz(15))
# Output: ['1', '2', 'Fizz', '4', 'Buzz', 'Fizz', '7', '8', 'Fizz', 'Buzz', '11', 'Fizz', '13', '14', 'FizzBuzz']
What this code does
- First we walk numbers from 1 through n.
- Then we test 15, then 3, then 5.
- Then we collect the labels.
Trap
Testing % 3 before % 15. Then 15 becomes "Fizz" and never "FizzBuzz".
Q-PY-058 Find the missing number in 1 to n
Answer
The numbers should be 1..n with one missing. The expected sum is n*(n+1)/2. Subtract the actual sum. XOR of all values also works without overflow worries in other languages.
Explanation
Ask whether the list is unsorted and whether n is len(nums) + 1. Do not sort unless you need to. A set difference is clear but uses extra memory.
Code
def missing_number(nums):
# n is the size of the full range, including the missing value
n = len(nums) + 1
# closed-form sum of 1..n
expected = n * (n + 1) // 2
# subtract what we actually have
return expected - sum(nums)
print(missing_number([1, 2, 4, 5]))
# Output: 3
What this code does
- First we figure out n from the list length plus one.
- Then we compute the sum that 1..n should have.
- Then we subtract the real sum to get the missing value.
Trap
Using n = max(nums) when n itself is the missing number, so the max is too small.
Q-PY-059 Invert a dictionary
Answer
Swap keys and values. If values are not unique, you must decide: last one wins, or collect a list of keys.
Explanation
{v: k for k, v in d.items()} is the unique-value case. For grouping "value -> [keys]", use defaultdict(list). Values must be hashable to become keys.
Code
from collections import defaultdict
def invert_unique(mapping):
# last key wins if a value repeats
return {value: key for key, value in mapping.items()}
def invert_group(mapping):
grouped = defaultdict(list)
for key, value in mapping.items():
# collect every key that had this value
grouped[value].append(key)
return dict(grouped)
print(invert_unique({"a": 1, "b": 2, "c": 1}))
# Output: {1: 'c', 2: 'b'}
print(invert_group({"a": 1, "b": 2, "c": 1}))
# Output: {1: ['a', 'c'], 2: ['b']}
What this code does
- First we swap keys and values with a comprehension.
- Then we show a grouping version that keeps every original key.
- Then we print both results so the clash behavior is visible.
Trap
Using the unique invert in production when values repeat, silently dropping keys.
Q-PY-060 Sort a list of dicts by a field
Answer
Use sorted(rows, key=lambda r: r["amount"], reverse=True). For two fields, return a tuple. Python's sort is stable.
Explanation
This is the pandas sort_values idea. Missing keys raise KeyError, so use .get if the schema is messy. Mixing None and numbers can TypeError on some Python versions.
Code
rows = [
{"product": "a", "amount": 10},
{"product": "b", "amount": 30},
{"product": "c", "amount": 20},
]
# sort high amount first
ranked = sorted(rows, key=lambda r: r["amount"], reverse=True)
print(ranked)
# Output: [{'product': 'b', 'amount': 30}, {'product': 'c', 'amount': 20}, {'product': 'a', 'amount': 10}]
What this code does
- First we start with unsorted product rows.
- Then we sort by
amountdescending. - Then we print the ranked list.
Trap
Calling rows.sort(key="amount"). key must be a function, not a string.
Q-PY-061 Deduplicate records by id, keep the last
Answer
Walk the rows in order. Store each row in a dict keyed by id. A later row overwrites an earlier one, so the last payload wins. Dict values then give one row per id.
Explanation
Event logs often replay the same id. "Keep last" means the latest payload wins. If you also need last-seen order in the output, rebuild from a list at the end, or use a dict and accept first-seen key order with last values. Interviewers care that you say which order you return.
Code
def keep_last_by_id(rows):
latest = {}
for row in rows:
# overwrite so the last payload for this id wins
latest[row["id"]] = row
# values() follows first-seen id order, with last payloads
return list(latest.values())
events = [
{"id": 1, "status": "new"},
{"id": 2, "status": "new"},
{"id": 1, "status": "done"},
]
print(keep_last_by_id(events))
# Output: [{'id': 1, 'status': 'done'}, {'id': 2, 'status': 'new'}]
What this code does
- First we key a dict by id.
- Then we overwrite with each later row for the same id.
- Then we return the dict values.
Trap
Filtering with a set of seen ids while walking forward, which keeps the first row, not the last.
Q-PY-062 Parse CSV text and filter rows
Answer
Use csv.DictReader. Do not split on commas. Filter with a list comprehension or a loop. Cast numbers yourself.
Explanation
StringIO stands in for a file. Production CSV needs encoding and a delimiter check. Empty fields are "", not None.
Code
import csv
from io import StringIO
raw = "country,amount\nUS,10\nIN,7\nUS,5\n"
# newline="" is the csv module's recommended file mode
handle = StringIO(raw)
reader = csv.DictReader(handle)
# keep US rows and turn amount into int
us_rows = []
for row in reader:
if row["country"] == "US":
us_rows.append({"country": row["country"], "amount": int(row["amount"])})
print(us_rows)
# Output: [{'country': 'US', 'amount': 10}, {'country': 'US', 'amount': 5}]
What this code does
- First we wrap CSV text in a file-like object.
- Then DictReader maps the header to each row.
- Then we keep US rows and convert amount to int.
Trap
line.split(",") on quoted fields, or comparing amount as a string so "9" > "10".
Q-PY-063 Longest substring without repeating characters
Answer
Use a sliding window and a dict of last seen indexes. Move the left edge past a repeat. Track the best window length.
Explanation
This is the classic two-pointer string question. O(n) time. If you restart from zero on every repeat, you miss a longer window in the middle.
Code
def longest_unique(s):
last_at = {}
left = 0
best = 0
for right, ch in enumerate(s):
# if this char is inside the current window, shrink from the left
if ch in last_at and last_at[ch] >= left:
left = last_at[ch] + 1
# record where we last saw this char
last_at[ch] = right
# window length is right - left + 1
best = max(best, right - left + 1)
return best
print(longest_unique("abcabcbb"))
# Output: 3
print(longest_unique("bbbbb"))
# Output: 1
What this code does
- First we grow a right pointer through the string.
- Then we jump the left pointer past a repeated character.
- Then we store the longest window we saw.
Trap
Using a set but only moving left by one without a loop, so the window still contains the duplicate.
Q-PY-064 Most common characters with Counter
Answer
Counter(text).most_common(n) returns the top n (char, count) pairs. That is the interview-speed answer.
Explanation
Good for "top error codes" too. most_common() with no n returns all, sorted by count. Ties follow first-seen order in CPython's Counter.
Code
from collections import Counter
def top_chars(text, n):
# skip spaces so we count letters
letters = [ch for ch in text.lower() if ch.isalpha()]
return Counter(letters).most_common(n)
print(top_chars("Mississippi", 3))
# Output: [('i', 4), ('s', 4), ('p', 2)]
What this code does
- First we keep letters and ignore other characters.
- Then we count them.
- Then we ask Counter for the top n.
Trap
Counting with a raw dict and then sorting the keys alphabetically instead of by count.
Q-PY-065 Top-k with heapq on a stream
Answer
Keep a min-heap of size k. Push a new value. If the heap grows past k, pop the smallest. The heap then holds the k largest.
Explanation
You do not need to store the whole stream sorted. This is how you take "top 10 amounts today" from a file. nlargest does this for you when the data is already a list.
Code
import heapq
def top_k(nums, k):
heap = []
for n in nums:
# push the new value
heapq.heappush(heap, n)
# if we have too many, drop the smallest
if len(heap) > k:
heapq.heappop(heap)
# sort high to low for a stable readable answer
return sorted(heap, reverse=True)
print(top_k([4, 1, 7, 3, 8, 2, 9], 3))
# Output: [9, 8, 7]
What this code does
- First we push each number onto a min-heap.
- Then we pop when the heap is larger than k, so only large values remain.
- Then we sort those k values high to low.
Trap
Using a max-heap mental model and popping the largest, which keeps the smallest k instead.
Q-PY-066 Group consecutive keys with itertools.groupby
Answer
Sort first if you want all equal keys together. Then groupby walks consecutive runs. Consume the group iterator inside the loop.
Explanation
This matches "runs in a log": many ERROR lines in a row. If the file is not sorted by the key, use a dict group instead. The group iterator is lazy and dies when you move to the next key.
Code
from itertools import groupby
rows = [
{"job": "load", "status": "ok"},
{"job": "load", "status": "retry"},
{"job": "extract", "status": "ok"},
{"job": "extract", "status": "ok"},
]
# sort so the same job sits in one consecutive run
rows.sort(key=lambda r: r["job"])
out = []
for job, group in groupby(rows, key=lambda r: r["job"]):
# list() because the group iterator is spent when the next key starts
items = list(group)
out.append((job, len(items)))
print(out)
# Output: [('extract', 2), ('load', 2)]
What this code does
- First we sort rows by job so equal keys are adjacent.
- Then groupby yields each consecutive job run.
- Then we count the rows in that run.
Trap
Forgetting list(group). Later you inspect group and it is empty.
Q-PY-067 Parse a status with match/case
Answer
match the payload. Use dict patterns for keys, and case _ for unknown shapes. This is cleaner than a long if/elif chain on Python 3.10+.
Explanation
Event routers and log severity maps are the real use. If the runtime is older than 3.10, this is a SyntaxError, so mention the version.
Code
def label_event(event):
match event:
# dict pattern: pull job when level is ERROR
case {"level": "ERROR", "job": job}:
return f"fail:{job}"
case {"level": "WARN"}:
return "warn"
case {"level": "INFO"}:
return "ok"
# default
case _:
return "unknown"
print(label_event({"level": "ERROR", "job": "load"}))
# Output: fail:load
print(label_event({"level": "DEBUG"}))
# Output: unknown
What this code does
- First we match the event dict against patterns.
- Then we bind
jobwhen the level is ERROR. - Then we return a label, including a default.
Trap
Writing case {"level": "ERROR"}: after a broader case that already ate the event, or forgetting case _.
Q-PY-068 Unpack nested records with `*`
Answer
Use first, *rest = row or nested unpacking. Use ** to merge dicts. Star unpacking fails if the length is wrong, so guard it.
Explanation
Log lines and CSV rows often have a head and a tail of extra fields. Catching extras is safer than hard indexes.
Code
row = ["load", "ERROR", "timeout", "retry=3"]
# first two fields are known, the rest is payload
job, level, *payload = row
print(job, level, payload)
# Output: load ERROR ['timeout', 'retry=3']
defaults = {"retries": 3, "region": "us"}
override = {"region": "eu"}
# ** unpacks dicts into a new dict, override wins
merged = {**defaults, **override}
print(merged)
# Output: {'retries': 3, 'region': 'eu'}
What this code does
- First we unpack a row into job, level, and leftover payload.
- Then we merge two dicts with
**. - Then we print both results.
Trap
job, level, payload = row with four items. That raises ValueError because the lengths do not match.
Q-PY-069 Moving average of a numeric list
Answer
Reuse the sliding-window sum. Divide each window sum by k. Round only at display time if the interviewer cares about decimals.
Explanation
This is a pandas rolling mean without pandas. Say if you want only full windows, or also partial tail windows. Full windows are simpler.
Code
def moving_average(nums, k):
if k <= 0 or k > len(nums):
return []
out = []
running = sum(nums[:k])
# first full window
out.append(running / k)
for i in range(k, len(nums)):
running += nums[i]
running -= nums[i - k]
out.append(running / k)
return out
print(moving_average([1, 2, 3, 4, 5], 3))
# Output: [2.0, 3.0, 4.0]
What this code does
- First we sum the first k values and divide by k.
- Then we slide the window with add and subtract.
- Then we append each average.
Trap
Dividing by len(nums) instead of k, or integer-dividing with // when a float mean is required.
Q-PY-070 Run-length encode a string
Answer
Walk the string. Count how many times the current character repeats. Emit the character and the count, then start a new run.
Explanation
"aaabbc" becomes "a3b2c1". Ask if a count of 1 should be omitted. Compression is only shorter when runs are long. This is not a general compressor.
Code
def compress(text):
if not text:
return ""
out = []
prev = text[0]
count = 1
# walk from the second character
for ch in text[1:]:
if ch == prev:
count += 1
else:
# close the old run
out.append(prev + str(count))
prev = ch
count = 1
# close the final run
out.append(prev + str(count))
return "".join(out)
print(compress("aaabbc"))
# Output: a3b2c1
What this code does
- First we start a run with the first character.
- Then we grow the count while the character stays the same.
- Then we flush each run, including the last one.
Trap
Forgetting to emit the last run after the loop ends.
Q-PY-071 Hash-join two lists of records
Answer
Build a dict from the smaller side keyed by the join key. Walk the other side and look up matches. This is a single-process inner join.
Explanation
Same idea as a Spark broadcast hash join, just in memory. If keys repeat, store lists in the map. If the map will not fit, use a real warehouse join.
Code
def inner_join(left, right, key):
# index the right side: key -> list of rows
index = {}
for row in right:
index.setdefault(row[key], []).append(row)
out = []
for row in left:
# skip if this key is not on the right
for other in index.get(row[key], []):
merged = dict(row)
# right columns overwrite on name clash, except we keep both via prefixes if needed
merged.update(other)
out.append(merged)
return out
users = [{"user_id": 1, "name": "Ada"}, {"user_id": 2, "name": "Bob"}]
events = [{"user_id": 1, "event": "login"}, {"user_id": 1, "event": "click"}]
print(inner_join(events, users, "user_id"))
# Output: [{'user_id': 1, 'event': 'login', 'name': 'Ada'}, {'user_id': 1, 'event': 'click', 'name': 'Ada'}]
What this code does
- First we index the user rows by user_id.
- Then we walk events and look up matching users.
- Then we merge each matching pair into one dict.
Trap
Using a dict of one row per key when the probe side has duplicates, which silently drops extra matches.
Q-PY-072 Running total / cumulative sum
Answer
Keep an accumulator. Add each value. Append the new total. That is a window SUM() OVER (ORDER BY ...) in Python.
Explanation
Useful for "balance after each transaction" and for simple CDF-style charts. Watch the first row. Empty input should return an empty list.
Code
def running_total(nums):
out = []
total = 0
for n in nums:
# add this value onto the accumulator
total += n
out.append(total)
return out
print(running_total([5, -2, 3, 1]))
# Output: [5, 3, 6, 7]
What this code does
- First we start the total at 0.
- Then we add each number in order.
- Then we store the total after every step.
Trap
Appending sum(nums[:i+1]) in a loop, which is O(n^2).
Q-PY-073 Output puzzle โ late-binding closures
Answer
A closure reads the variable at call time, not at definition time. A loop variable is one variable. All lambdas see the last value unless you bind a default argument.
Explanation
This is why functions.append(lambda: i) in a for-loop is a trap. Default args bind at definition. In production, use functools.partial or a real def with a parameter.
Code
funcs = []
for i in range(3):
# each lambda looks up i later, not now
funcs.append(lambda: i)
print([fn() for fn in funcs])
# Output: [2, 2, 2]
fixed = []
for i in range(3):
# default arg n=i captures the value at this loop step
fixed.append(lambda n=i: n)
print([fn() for fn in fixed])
# Output: [0, 1, 2]
What this code does
- First we store lambdas that read the loop variable
i. - Then we call them after the loop, so they all see
i == 2. - Then we bind
n=ias a default so each function keeps its own number.
Trap
Blaming the lambda keyword itself. A nested def has the same late-binding behavior.
No questions match. Clear search or pick All.