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 theory easy

Answer

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.

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? theory easy

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 `==`? theory easy

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 theory medium

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? theory medium

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`? theory easy

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? theory medium

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)? theory hard

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? theory medium

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? theory medium

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? theory easy

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? theory medium

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 theory hard

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? theory hard

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__`? theory hard

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? theory medium

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? theory medium

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? theory medium

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? theory easy

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? theory easy

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? theory easy

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`? theory easy

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? theory medium

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? theory medium

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 `:=`? theory medium

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`? theory medium

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? theory easy

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? theory easy

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? theory medium

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? theory medium

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? theory easy

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? theory medium

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()`? theory easy

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? theory easy

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? theory easy

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 coding easy

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

python โ€” editable
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

  1. First we split the text into a list of words.
  2. Then we reverse that list.
  3. 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 coding easy

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

python โ€” editable
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

  1. First we lower-case both strings and remove spaces.
  2. Then we count characters in each string.
  3. 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 coding medium

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

python โ€” editable
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

  1. First we make an empty dict of seen values.
  2. Then we compute the partner each number would need.
  3. 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 coding medium

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

python โ€” editable
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

  1. First we put the top-level items on a stack.
  2. Then we pop one item at a time.
  3. 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 coding easy

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

python โ€” editable
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

  1. First we lower-case the text.
  2. Then we split it into words.
  3. 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 coding easy

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

python โ€” editable
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

  1. First we walk the dict as (key, count) pairs.
  2. Then we ask the heap for the three largest counts.
  3. 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 coding easy

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

python โ€” editable
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

  1. First we copy the base dict.
  2. Then we write override keys on top.
  3. 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 coding medium

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

python โ€” editable
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

  1. First we compile a regex with timestamp, level, and message groups.
  2. Then we match each line.
  3. 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 coding medium

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

python โ€” editable
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

  1. First we sum the first k numbers.
  2. Then we slide one step: add the new right value and subtract the old left value.
  3. 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 coding medium

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

python โ€” editable
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

  1. First we make a dict that defaults missing keys to 0.
  2. Then we add each row's amount into its country.
  3. 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 coding medium

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

python โ€” editable
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

  1. First we wrap the function with @timed.
  2. Then we start a clock, call the real function, and stop the clock.
  3. 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 coding medium

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

python โ€” editable
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

  1. First we yield lines from the text.
  2. Then we drop blanks, split out the level, and keep ERROR rows.
  3. Then we materialize the last generator with list so 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 coding easy

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

python โ€” editable
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

  1. First we loop each line from the file-like object.
  2. Then we strip whitespace and skip empty lines.
  3. 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 coding easy

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

python โ€” editable
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

  1. First we make an empty set of values we have already kept.
  2. Then we append an item only when it is new.
  3. 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 coding easy

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

python โ€” editable
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

  1. First we walk each number while holding the top and second-top unique values.
  2. Then we promote the old maximum when we see a bigger number.
  3. 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 coding easy

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

python โ€” editable
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

  1. First we drop spaces and punctuation and lower-case the rest.
  2. Then we reverse that cleaned string.
  3. 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 coding easy

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

python โ€” editable
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

  1. First we start with 0 and 1.
  2. Then we yield the left number n times.
  3. 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 coding easy

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

python โ€” editable
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

  1. First we reject a non-positive chunk size.
  2. Then we walk start indexes 0, 2, 4, ...
  3. 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 coding medium

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

python โ€” editable
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

  1. First we look at the current object type.
  2. Then we recurse into dict keys and list indexes while building a path.
  3. 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 coding medium

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

python โ€” editable
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

  1. First we append into a default list that lives on the function object.
  2. Then we call it three times and see the list grow.
  3. Then we show the None sentinel 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 coding medium

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

python โ€” editable
# 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

  1. First we build a matrix with [[]] * 3, which repeats one inner list.
  2. Then we append to the first row and see all rows change.
  3. 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 coding easy

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

python โ€” editable
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

  1. First we walk numbers from 1 through n.
  2. Then we test 15, then 3, then 5.
  3. 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 coding easy

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

python โ€” editable
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

  1. First we figure out n from the list length plus one.
  2. Then we compute the sum that 1..n should have.
  3. 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 coding easy

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

python โ€” editable
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

  1. First we swap keys and values with a comprehension.
  2. Then we show a grouping version that keeps every original key.
  3. 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 coding easy

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

python โ€” editable
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

  1. First we start with unsorted product rows.
  2. Then we sort by amount descending.
  3. 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 coding medium

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

python โ€” editable
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

  1. First we key a dict by id.
  2. Then we overwrite with each later row for the same id.
  3. 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 coding easy

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

python โ€” editable
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

  1. First we wrap CSV text in a file-like object.
  2. Then DictReader maps the header to each row.
  3. 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 coding hard

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

python โ€” editable
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

  1. First we grow a right pointer through the string.
  2. Then we jump the left pointer past a repeated character.
  3. 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 coding easy

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

python โ€” editable
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

  1. First we keep letters and ignore other characters.
  2. Then we count them.
  3. 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 coding medium

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

python โ€” editable
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

  1. First we push each number onto a min-heap.
  2. Then we pop when the heap is larger than k, so only large values remain.
  3. 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 coding medium

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

python โ€” editable
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

  1. First we sort rows by job so equal keys are adjacent.
  2. Then groupby yields each consecutive job run.
  3. 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 coding medium

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

python โ€” editable
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

  1. First we match the event dict against patterns.
  2. Then we bind job when the level is ERROR.
  3. 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 `*` coding easy

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

python โ€” editable
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

  1. First we unpack a row into job, level, and leftover payload.
  2. Then we merge two dicts with **.
  3. 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 coding medium

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

python โ€” editable
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

  1. First we sum the first k values and divide by k.
  2. Then we slide the window with add and subtract.
  3. 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 coding medium

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

python โ€” editable
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

  1. First we start a run with the first character.
  2. Then we grow the count while the character stays the same.
  3. 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 coding medium

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

python โ€” editable
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

  1. First we index the user rows by user_id.
  2. Then we walk events and look up matching users.
  3. 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 coding easy

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

python โ€” editable
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

  1. First we start the total at 0.
  2. Then we add each number in order.
  3. 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 coding hard

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

python โ€” editable
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

  1. First we store lambdas that read the loop variable i.
  2. Then we call them after the loop, so they all see i == 2.
  3. Then we bind n=i as a default so each function keeps its own number.

Trap

Blaming the lambda keyword itself. A nested def has the same late-binding behavior.