
Pattern Path
Python: one problem shape at a time
The Python that data engineering interviews ask for, with the algorithms branch kept optional. Each step teaches one shape with an anchor problem first, then variations that reinforce it and twists that break the naive version.
0 of 110 problems solved cleanly across 12 steps.
01Parse and clean text
Parse and clean0 of 12 solved cleanly
Read first: Strings and text, Regular expressions for logs
How to recognise this pattern
Look for
- A line of text that must become fields
- Trim, lowercase or standardise before comparing
- Lines with a missing or bad field
Smallest shape
def parse_line(line): # strip each piece, then unpack into three names sku, qty, price = [part.strip() for part in line.split("|")] return {"sku": sku, "qty": int(qty), "price": float(price)}The usual trap
split keeps empty fields. 'A1||9.5'.split('|') gives ['A1', '', '9.5'], and int('') raises ValueError: invalid literal for int() with base 10: ''. Decide what a missing field means before you convert it.
Not this
If the pattern is irregular (many formats in 1 column), a regular expression fits better than split. Use split when one separator always divides the same fields.
- Parse a delimited inventory lineAnchorFree10m
- Validate a discount code formatVariationFree10m
- Reverse words in a log messageVariationFree10m
- Bigrams from a sentenceVariationFree8m
- Longest common prefixVariationFree8m
- Normalize a batch of phone numbersVariationPro15m
- Parse a fixed-width mainframe extractVariationPro15m
- Top error codes from log linesVariationPro14m
- Forward-fill NoneVariationFree8m
- Compact a run-length encoded event logVariationFree10m
- Expand a run-length encoded shift logVariationFree10m
- Safely parse a batch of numeric stringsTwistPro15m
02Dictionaries and counting
Count and group0 of 14 solved cleanly
Read first: DE data structures, Hash maps & frequency counting
How to recognise this pattern
Look for
- How many times does each thing appear
- Group items by a key
- Find a record by its id
Smallest shape
def count_words(text): counts = {} for word in text.lower().split(): counts[word] = counts.get(word, 0) + 1 return countsThe usual trap
A defaultdict creates a key when you only look. After d = defaultdict(int), d['a'] += 1, and then reading d['zzz'], len(d) is 2 and the dict is {'a': 1, 'zzz': 0}. Use `in` or .get when you only want to check.
Not this
If you only need to know whether you have seen an item, a set is enough. A dict is for when each key carries a value, such as a count or a list.
- Word Frequency CounterAnchorFree10m
- First Occurrence MapVariationFree10m
- Most common word in a support ticketVariationFree10m
- Counter for warehouse zone visitsVariationFree10m
- Word frequency ignoring stop wordsVariationPro15m
- Group orders by customer and statusVariationPro15m
- Shared tags between two campaignsVariationFree10m
- Top K WordsVariationFree10m
- Group-by on CSV text without pandasVariationPro16m
- Hash join two lists of dictsVariationPro16m
- Chunk transactions into daily batches by cutoff timeVariationPro15m
- Daily per-user event countsTwistPro16m
- Diff 2 versions of a product catalogTwistPro20m
- Most common comment, counted once per locationTwistPro12m
How to recognise this pattern
Look for
- Sort records by one field, then another
- Break ties alphabetically
- The second largest, the closest, the top few
Smallest shape
def sort_products(rows): # sort by price, then by name when prices tie return sorted(rows, key=lambda r: (r["price"], r["name"]))The usual trap
reverse=True reverses the tie-break too. For prices 5, 5, 9 and names b, a, c, sorting by (price, name) with reverse=True gives c, b, a. A descending price with an ascending name needs the key (-price, name), which gives c, a, b.
Not this
If you only need the single biggest or smallest, max or min with a key is enough and does not sort the whole list.
- Sort products by price then nameAnchorFree10m
- Custom sort by multiple derived keysVariationPro15m
- Second largest order totalVariationFree10m
- Rotate a shift scheduleVariationFree10m
- Even totals from a mixed order listVariationFree10m
- Coordinates as immutable recordsVariationFree10m
- Smallest number from odd digitsVariationFree8m
- Top-K most active accountsVariationPro15m
How to recognise this pattern
Look for
- Remove duplicates but keep the order
- Have I seen this before
- 2 items that add up to a target
Smallest shape
def unique_in_order(items): seen = set() out = [] for item in items: if item not in seen: seen.add(item) out.append(item) return outThe usual trap
Set members must be hashable (in practice, unchangeable, like numbers, strings and tuples). set([[1], [2]]) raises TypeError: unhashable type: 'list'. Turn lists into tuples first. And list(set(items)) throws away the order.
Not this
Counting how often things appear is a dictionary job. Use a set when the only question is yes or no, and a dict when each key needs a value.
- Remove Duplicates Preserving OrderAnchorFree10m
- Contains DuplicateVariationFree10m
- Two SumVariationFree10m
- Find IntersectionVariationFree10m
- Valid AnagramVariationFree10m
- Group AnagramsVariationFree10m
- Anagram grouping of product SKUsVariationPro15m
- First non-repeating character in a request idVariationPro15m
- Majority ElementVariationFree10m
- Longest Consecutive SequenceVariationFree10m
- Find All DuplicatesVariationFree10m
- Find DisappearedVariationFree10m
- Find Duplicate NumberVariationFree10m
- Collapse Consecutive DuplicatesVariationFree10m
- Two-account transfer matchingVariationPro15m
- Deduplicate customer records by fuzzy keyTwistPro15m
- Latest valid location per driverTwistPro16m
- Skip already-committed messages after a replayTwistPro12m
How to recognise this pattern
Look for
- Two sorted lists to combine
- Walk two lists, or both ends of one list
- One pass, no re-sorting
Smallest shape
def merge_sorted(a, b): i = j = 0 out = [] while i < len(a) and j < len(b): if a[i] <= b[j]: out.append(a[i]) i += 1 else: out.append(b[j]) j += 1 return out + a[i:] + b[j:]The usual trap
The loop stops when one list runs out, and the other still has items. Without the last line's a[i:] + b[j:], merge_sorted([1, 4, 9], [2, 3]) returns [1, 2, 3] and drops 4 and 9.
Not this
A sliding window keeps a block of neighbouring values that moves along one list. Two pointers are two separate positions, over two sorted lists or from both ends of one list.
- Merge Two Sorted ListsAnchorFree10m
- 3SumVariationFree10m
- Container With Most WaterVariationFree10m
- Trapping Rain WaterVariationPro10m
- Valid PalindromeVariationPro14m
- Negatives before non-negativesVariationFree8m
How to recognise this pattern
Look for
- Best 3-day or 7-day stretch
- A block of neighbouring values that moves along the data
Smallest shape
def best_window(values, k): window = sum(values[:k]) best = window for i in range(k, len(values)): window += values[i] - values[i - k] best = max(best, window) return bestThe usual trap
Off by one on the last window. For [3, 1, 4, 1, 5, 9, 2, 6] and k=3, taking max(sum(values[i:i + 3]) for i in range(len(values) - 3)) misses the last window and returns 16. The correct answer is 17, from 9 + 2 + 6.
Not this
Prefix sums (running totals stored in a list) answer many range questions after one pass. A sliding window is for one pass when the window moves in a single direction.
- Best 3-day sales windowAnchorFree15m
- Longest streak of active daysVariationPro15m
- Longest Substring No RepeatVariationPro14m
- Longest Substring K DistinctVariationPro14m
- Longest Repeating Char ReplaceVariationPro14m
- Min Window SubstringVariationPro14m
- Permutation in StringVariationPro14m
- Find All AnagramsVariationPro14m
- Sliding Window MaxVariationPro14m
- Sliding window unique visitor countVariationPro20m
- Contains DuplicateCheckpointFree10m
07Prefix sums and running state
Running state0 of 8 solved cleanly
Read first: Prefix sums & range aggregates
How to recognise this pattern
Look for
- Running total
- Sum of any range, asked many times
- Carry a value forward as you scan
Smallest shape
from itertools import accumulate def running_totals(values): return list(accumulate(values))The usual trap
Range sums need a leading 0. With values [5, 3, 8, 2], the totals are [5, 8, 16, 18]. To get the sum from index l to r you compute prefix[r] - prefix[l - 1]. For l = 0 that reads prefix[-1], the last item, and gives -2 instead of 16. Start the prefix list with 0 and use prefix[r + 1] - prefix[l].
Not this
A sliding window recomputes only what enters and leaves a fixed block. Prefix sums store totals so any block can be answered later.
- Running sum of daily signupsAnchorFree10m
- Product Except SelfVariationFree10m
- Max Subarray SumVariationFree10m
- Best Time Buy SellVariationFree10m
- Find Missing NumberVariationFree10m
- Find the pivot index of a sales listVariationPro15m
- Subarray Sum Equals KTwistPro14m
- Best 3-day sales windowCheckpointFree15m
08Intervals and merging ranges
Intervals0 of 7 solved cleanly
Read first: Sorting & binary search with bisect
How to recognise this pattern
Look for
- Overlapping time ranges
- Merge windows into the fewest
- Do these two bookings clash
Smallest shape
def merge_intervals(intervals): out = [] for start, end in sorted(intervals): if out and start <= out[-1][1]: out[-1][1] = max(out[-1][1], end) else: out.append([start, end]) return outThe usual trap
Whether touching ranges merge is a rule you must ask about. With [(1, 2), (2, 3)], comparing start <= previous end gives [[1, 3]], and start < previous end gives [[1, 2], [2, 3]]. Also sort first: [(5, 6), (1, 3), (2, 4)] only merges correctly once it is sorted.
Not this
If the ranges are already sorted and you are combining two lists, two pointers is enough. Intervals need a sort when the input order is unknown.
- Merge IntervalsAnchorPro14m
- Insert IntervalVariationPro14m
- Non-Overlapping IntervalsVariationPro14m
- Meeting RoomsVariationPro14m
- Meeting Rooms IIVariationPro14m
- Interval merge for maintenance windowsVariationPro20m
- Merge Two Sorted ListsCheckpointFree10m
How to recognise this pattern
Look for
- A list inside a list, to any depth
- A function that has to call itself (recursion)
- Collect every value of one kind from nested data
Smallest shape
def flatten(items): out = [] for item in items: if isinstance(item, list): out.extend(flatten(item)) else: out.append(item) return outThe usual trap
A string is iterable, and each character is again a string. If you test for 'can I loop over it' instead of isinstance(item, list), flatten([1, ['ab']]) never reaches a plain value and fails with RecursionError.
Not this
If the structure is always one level deep, a list comprehension is simpler. Use recursion when the depth is not known.
- Flatten Nested ListsAnchorFree10m
- Flatten nested JSON to dotted keysVariationPro14m
- Unflatten dotted keysVariationPro16m
- Extract all email addresses from a nested payloadVariationPro15m
- Flatten a directory listing into pathsVariationPro15m
- Validate a nested order schemaVariationPro15m
- Merge two JSON-like config dicts with overridesVariationPro15m
- Infer a schema from JSON recordsTwistPro18m
10Generators and batching
Lazy streams0 of 7 solved cleanly
Read first: Iterators & generators (yield)
How to recognise this pattern
Look for
- Too much data to hold in memory
- Process in batches of a fixed size
- Read one item at a time
Smallest shape
from itertools import islice def batches(iterable, size): iterator = iter(iterable) while True: batch = list(islice(iterator, size)) if not batch: return # the stream is empty, so the generator ends yield batch # hand back one batch, then pause until asked againThe usual trap
A generator can be read only once. After g = (x * x for x in range(3)), list(g) is [0, 1, 4] and a second list(g) is []. If you need the data twice, store it or build the generator again.
Not this
A list is better when the data is small and you need it more than once. A generator is for one pass over data that may not fit in memory.
- Batch a stream with a size limitAnchorPro15m
- Lazily read and filter a large log iteratorVariationPro15m
- Merge K sorted log files lazilyVariationPro16m
- Process print jobs in arrival orderVariationFree10m
- Reservoir samplingVariationPro20m
- Weighted round-robin task schedulerVariationPro20m
- Tumbling-window counts with a watermarkTwistPro18m
11Retries, decorators and context managers
Reliable helpers0 of 7 solved cleanly
Read first: Retries, backoff, and idempotency, Context managers & resource cleanup, Logging instead of print-debugging
How to recognise this pattern
Look for
- Run this step again if it fails
- Add timing or logging around many functions
- Add the same behaviour around many functions
Smallest shape
import functools import time def retry(times, delay=0.0): def decorator(func): @functools.wraps(func) def wrapper(*args, **kwargs): for attempt in range(1, times + 1): try: return func(*args, **kwargs) except ConnectionError: if attempt == times: raise time.sleep(delay) return wrapper return decoratorThe usual trap
Without functools.wraps, the decorated function takes the wrapper's name. After a decorator written without it, the wrapped function's __name__ is the wrapper's name ('w' in our check) instead of 'load', which makes logs and tracebacks hard to read. Also retry only temporary errors such as ConnectionError, not every Exception.
Not this
A decorator wraps a whole function, so the same behaviour applies to every call. To guard one block with cleanup that always runs, even on an error, use a with statement and a context manager.
- Time a function's executionAnchorFree10m
- Retry a flaky operation with a capVariationPro15m
- Memoize an expensive lookup functionVariationPro15m
- Context manager for a scoped transaction logVariationPro20m
- Model an inventory item with validationVariationPro15m
- Token-bucket rate limiterVariationPro16m
- A simple LRU-style access cacheVariationPro20m
12Small pipeline helpers from real tickets
Production tickets0 of 7 solved cleanly
Read first: Backfills and catchup, Consumer groups, Dockerfile bugs: USER root
How to recognise this pattern
Look for
- A job must rerun for every date it missed (a backfill)
- Which dates, partitions or messages a job should handle
- A small exact rule behind a pipeline ticket
Smallest shape
from datetime import date, timedelta def dates_between(start, end): return [start + timedelta(days=d) for d in range((end - start).days + 1)]The usual trap
Ranges are easy to get one short. For 2026-10-01 to 2026-10-04, (end - start).days is 3, but a backfill must cover 4 dates, so add 1 when both ends are included. State whether the end is included before writing the loop.
Not this
If the task is about the order jobs must run in (job A before job B), it is a different problem from which dates to run. Read which one the ticket asks for.
- Find the dates a backfill needs to coverAnchorFree12m
- Translate a service name across AWS, Azure, and GCPVariationFree10m
- Assign partitions across a consumer groupVariationFree12m
- CI pipeline: order the stages for a safe deployVariationFree10m
- Pick a storage tier from access ageVariationPro12m
- Dockerfile review: catch a container running as rootVariationPro12m
- Order DAG tasks so every dependency runs firstVariationPro15m
Optional: algorithms branch (7 steps)
Binary search, stacks, heaps, linked lists, trees, graphs and dynamic programming. Useful for some interviews, not needed to follow the main path.
- First and last position in a sorted listAnchorPro14m
- Search in a rotated sorted arrayVariationPro16m
- Time-based key-value storeVariationPro16m
- Capacity to ship within D daysVariationPro16m
- Valid ParenthesesAnchorFree10m
- StackVariationPro14m
- QueueVariationPro14m
- Min StackVariationPro14m
- Eval RPNVariationPro14m
- Daily TemperaturesVariationPro14m
- Decode stringVariationPro16m
- Match opening and closing tagsVariationPro15m
- Kth largest elementAnchorPro14m
- MedianFinderVariationPro18m
- Top K FrequentVariationPro18m
- Merge K ListsVariationPro18m
- Task scheduler with cooldownVariationPro16m
- Balanced team assignment by workloadVariationPro20m
- Reverse Linked ListAnchorPro14m
- Merge Two Sorted ListsVariationPro14m
- Linked List CycleVariationPro14m
- Middle of Linked ListVariationPro14m
- Remove Nth From EndVariationPro14m
- Reorder ListVariationPro14m
- Copy Random ListVariationPro14m
- Max DepthAnchorPro18m
- Invert Binary TreeVariationPro18m
- Same TreeVariationPro18m
- SubtreeVariationPro18m
- LCA BSTVariationPro18m
- Level OrderVariationPro18m
- Validate BSTVariationPro18m
18Graphs and dependencies
BFS and DFS0 of 10 solved cleanly
Read first: BFS, DFS & DAG traversals, Union-find & connected components
- Number of IslandsAnchorPro18m
- Clone GraphVariationPro18m
- Course ScheduleVariationPro18m
- Connected ComponentsVariationPro18m
- Graph Valid TreeVariationPro18m
- Rotting orangesVariationPro18m
- Network delay timeVariationPro18m
- Accounts mergeVariationPro18m
- Detect a cycle in a task dependency mapVariationPro20m
- Topological build order for servicesVariationPro20m
- Climbing StairsAnchorPro18m
- Coin ChangeVariationPro18m
- House RobberVariationPro18m
- Word breakVariationPro20m
- Longest increasing subsequenceVariationPro20m
- Longest common subsequenceVariationPro16m