CoderPad Interview Questions: 8 Practice Problems with Follow-Ups
These are 8 original practice problems built for a live CoderPad pad with an interviewer watching: each one starts small and grows with follow-ups such as "now skip malformed lines" or "now make it fast". Each has its parts in order, examples, talking points to say out loud, a tested Python 3 solution and what the interviewer is likely to probe. CoderPad itself has no official question bank, because each company writes its own interview questions, so practice the patterns and the talking.
CoderPad Interview is a live shared editor. You write and run code while the company's interviewer watches, with built-in video and a whiteboard.
Each company writes its own questions, and the interviewer may add follow-ups as you go.
Many interviews start with 5 minutes of Focus Time to read the problem. Running code is usually on, but some companies turn it off.
Every keystroke is recorded for playback, and pastes and leaving the tab are flagged. Companies can turn on CoderPad's own AI assist, and every prompt is saved for them to review.
Scoring, proctoring and retakes are in our CoderPad guide.
The 8 CoderPad practice problems
Solve Part 1 first and run it, then take each follow-up as if the interviewer just asked it. The solutions show the final version, which handles every part.
You get web server log lines like "GET /api/users 200 35": the method, the path, the status code and the response time in milliseconds.
Part 1. Write count_by_path(lines), which returns a dict from each path to the number of requests it received.
Part 2. Now write top_paths(lines, k), which returns the k paths with the most requests. Break ties alphabetically by path.
Part 3. Now some lines are malformed (the wrong number of fields, or a status or time that is not a number). Skip them instead of crashing, and write p95_by_path(lines) with each path's 95th-percentile response time by the nearest-rank method: sort the times and take the value at position ceil(0.95 x count), counting from 1.
The last two lines are malformed and skipped. The tie at 2 requests goes to "/api/users" alphabetically.
Show approach and solution
Say this out loud
Ask what a malformed line can look like before you write the parser.
Put the parsing in one helper so every part shares the same rules.
Use integer math for the rank, (95 x n + 99) // 100, because floating point can make ceil(0.95 x n) off by one.
Approach
Write one parse helper that returns (path, milliseconds) or None for a bad line. Part 1 counts paths with a Counter. Part 2 sorts the paths by (negative count, path) and slices k. Part 3 groups the times per path, sorts each list once and picks the nearest-rank value.
Python 3 solution
from collections import Counter, defaultdict
def parse(line):
parts = line.split()
if len(parts) != 4:
return None
_, path, status, ms = parts
if not status.isdigit() or not ms.isdigit():
return None
return path, int(ms)
def count_by_path(lines):
counts = Counter()
for line in lines:
record = parse(line)
if record:
counts[record[0]] += 1
return dict(counts)
def top_paths(lines, k):
counts = count_by_path(lines)
return sorted(counts, key=lambda path: (-counts[path], path))[:k]
def p95_by_path(lines):
times = defaultdict(list)
for line in lines:
record = parse(line)
if record:
times[record[0]].append(record[1])
result = {}
for path, values in times.items():
values.sort()
rank = (95 * len(values) + 99) // 100
result[path] = values[rank - 1]
return result
Complexity: O(n log n) time in the worst case for the sorts, O(n) space.
What the interviewer will probe: Empty input, every line malformed, k larger than the number of paths, a path with one request (its p95 is that request), and ties in the counts.
Problem 2MediumSliding window + deque
API rate limiter
class RateLimiter: __init__(limit, window), allow(user, t), retry_after(user, t), sweep(t)
Build a rate limiter. RateLimiter(limit, window) lets each user make at most limit requests in any window seconds. A request made at time s still counts at time t if t - s < window. Calls come with non-decreasing timestamps, and rejected requests do not count toward the limit.
Part 1. Implement allow(user, t), which returns True and records the request if the user is under the limit, otherwise False.
Part 2. Now the service has millions of users. Make sure users who have gone quiet do not hold memory forever.
Part 3. Now add retry_after(user, t): how many seconds until this user's next request would be allowed, or 0 if it would be allowed now.
At t = 11 the request from t = 1 has expired. At t = 12 the user holds requests from 5 and 11, so the next slot opens at 15.
Show approach and solution
Say this out loud
Ask whether the window slides or resets on fixed boundaries. This solution slides.
Explain why rejected requests are not stored: they should not push the window forward.
For millions of users, a periodic sweep (or a store with expiring keys) frees quiet users; mention fixed time buckets as a cheaper, less exact option.
Approach
Keep a deque of accepted timestamps per user. On each call, drop timestamps that are window seconds old or more; if fewer than limit remain, accept and append. Users whose deque empties are deleted, and sweep(t) clears users whose newest request has expired. For retry_after, a full user waits until the oldest kept request expires.
Python 3 solution
from collections import deque
class RateLimiter:
def __init__(self, limit, window):
self.limit = limit
self.window = window
self.requests = {}
def _recent(self, user, t):
queue = self.requests.get(user)
if queue is None:
return deque()
while queue and t - queue[0] >= self.window:
queue.popleft()
if not queue:
del self.requests[user]
return deque()
return queue
def allow(self, user, t):
queue = self._recent(user, t)
if len(queue) >= self.limit:
return False
queue.append(t)
self.requests[user] = queue
return True
def retry_after(self, user, t):
queue = self._recent(user, t)
if len(queue) < self.limit:
return 0
return queue[0] + self.window - t
def sweep(self, t):
quiet = [user for user, queue in self.requests.items() if t - queue[-1] >= self.window]
for user in quiet:
del self.requests[user]
Complexity: allow and retry_after are amortized O(1), since each timestamp is added and removed once. sweep is O(users). Memory is O(active users x limit).
What the interviewer will probe: Requests exactly window seconds apart (the old one has expired), many users at once, a user who returns after a long gap, and retry_after for a user under the limit.
Problem 3MediumTwo stacks
Text editor with undo and redo
class Editor: append(text), delete(k), undo(), redo(), text()
Build a tiny text editor that holds one string, starting empty.
Part 1. Support append(text), which adds text at the end, delete(k), which removes the last k characters (or everything if there are fewer), and text(), which returns the current string.
Part 2. Now add undo(), which reverses the most recent append or delete that has not been undone. With nothing to undo, it does nothing.
Part 3. Now add redo(), which re-applies the most recently undone change. Any new append or delete clears the redo history.
The new append clears the redo history, so the last redo does nothing.
Show approach and solution
Say this out loud
Store what a delete actually removed, not just k, so undo can put it back.
Ask whether undo should group fast typing into one step, as real editors do.
A list of characters avoids copying the whole string on every edit.
Approach
Keep the text as a list of characters plus two stacks of changes, each change stored as (kind, text). An undo applies the opposite change and moves it to the redo stack; a redo re-applies it and moves it back. Every new edit clears the redo stack.
Complexity: append is O(len(text)), delete is O(k), and undo or redo is O(size of the change). text() is O(n).
What the interviewer will probe: Deleting more than the whole text and then undoing, undo and redo with empty stacks, redo after a new edit (must do nothing), and long mixed sequences.
Problem 4MediumRecursion on nested data
Flatten and unflatten a config
def flatten(config), def unflatten(flat)
Services load settings from nested JSON. Turn a nested config into a flat dict whose keys are joined with dots, and back.
Part 1. Write flatten(config) for nested dicts: {"db": {"host": "x", "port": 5432}} becomes {"db.host": "x", "db.port": 5432}.
Part 2. Now values can be lists too. Use the index as the key part, so {"hosts": ["a", "b"]} becomes {"hosts.0": "a", "hosts.1": "b"}. Lists can hold dicts and dicts can hold lists. Keep empty dicts and lists as values.
Part 3. Now write unflatten(flat), which rebuilds the original. A parent whose child keys are exactly 0, 1, 2 ... becomes a list.
Ask what should happen to empty dicts and lists. Keeping them as values lets unflatten restore them.
Point out that keys containing dots, or dict keys that look like numbers, make the flat form ambiguous.
Recursion depth equals nesting depth; mention an explicit stack if configs can be very deep.
Approach
flatten walks dict items or list indexes, builds the dotted path, and recurses into non-empty dicts and lists; everything else is a leaf. unflatten splits each key on dots and builds nested dicts, then a second pass turns any dict whose keys are exactly 0 to n - 1 into a list.
Python 3 solution
def flatten(config, prefix=''):
flat = {}
items = config.items() if isinstance(config, dict) else enumerate(config)
for key, value in items:
path = f'{prefix}.{key}' if prefix else str(key)
if isinstance(value, (dict, list)) and value:
flat.update(flatten(value, path))
else:
flat[path] = value
return flat
def unflatten(flat):
root = {}
for path, value in flat.items():
node = root
parts = path.split('.')
for part in parts[:-1]:
node = node.setdefault(part, {})
node[parts[-1]] = value
return to_lists(root)
def to_lists(node):
if not isinstance(node, dict):
return node
fixed = {key: to_lists(value) for key, value in node.items()}
if fixed and all(key.isdigit() for key in fixed):
indexes = sorted(int(key) for key in fixed)
if indexes == list(range(len(indexes))):
return [fixed[str(i)] for i in indexes]
return fixed
Complexity: O(total keys and values) for both directions, plus sorting list indexes.
What the interviewer will probe: Empty dicts and lists, lists of dicts, top-level values that are not nested, deep nesting, and a round trip of flatten then unflatten.
Confirm whether meetings that touch (one ends at 10:00, the next starts at 10:00) leave a gap. Here they do not.
Merging everyone's meetings first turns many calendars into one simple problem.
Sorting dominates the cost, so say O(m log m) for m meetings in total.
Approach
Sort the meetings and merge each one into the last if it starts at or before the last one's end. For free slots, merge all calendars together, then walk the merged list with a cursor that starts at day_start: any meeting that starts after the cursor leaves a gap, and the cursor jumps to the meeting's end. Clip to the day and filter by duration.
Python 3 solution
def merge(busy):
merged = []
for start, end in sorted(busy):
if merged and start <= merged[-1][1]:
merged[-1][1] = max(merged[-1][1], end)
else:
merged.append([start, end])
return merged
def free_slots(calendars, day_start, day_end, duration=0):
everyone = merge([meeting for calendar in calendars for meeting in calendar])
slots = []
cursor = day_start
for start, end in everyone:
if cursor >= day_end:
break
if start > cursor:
slots.append([cursor, min(start, day_end)])
cursor = max(cursor, end)
if cursor < day_end:
slots.append([cursor, day_end])
return [slot for slot in slots if slot[1] - slot[0] >= duration]
Complexity: O(m log m) time for m meetings in total, O(m) space.
What the interviewer will probe: No meetings at all (the whole day is free), meetings that start before the day or end after it, meetings nested inside longer ones, and a duration longer than any gap.
A city grid shows power status: 1 means a block has no power and 0 means it is fine. Blocks without power that touch up, down, left or right form one outage.
Part 1. Write count_outages(grid), which returns how many separate outages there are.
Part 2. Now write largest_outage(grid), which returns the size of the largest outage in blocks (0 if there is none).
Part 3. Now the city starts with power everywhere and blocks lose power one at a time. Given a list of [row, col] events, return the number of outages after each one. Aim for close to constant time per event, not a fresh scan.
Block (0, 1) joins the two outages into one. Reporting it again changes nothing.
Show approach and solution
Say this out loud
Use an explicit queue, not recursion: a big grid can hit Python's recursion limit.
Ask whether diagonal neighbors count. Here they do not.
For Part 3, explain why union-find beats rescanning: each event only touches 4 neighbors.
A repeated event for the same block must not change the count.
Approach
Parts 1 and 2 flood-fill each unvisited outage block with breadth-first search and record the size of each outage. Part 3 keeps a union-find over the blocks that have lost power: a new block adds one outage, and each union with a neighbor in a different set removes one. Union by size plus path halving keeps each operation nearly constant.
Python 3 solution
from collections import deque
def outage_sizes(grid):
rows, cols = len(grid), len(grid[0])
seen = [[False] * cols for _ in range(rows)]
sizes = []
for r in range(rows):
for c in range(cols):
if grid[r][c] == 1 and not seen[r][c]:
seen[r][c] = True
queue = deque([(r, c)])
size = 0
while queue:
cr, cc = queue.popleft()
size += 1
for nr, nc in ((cr + 1, cc), (cr - 1, cc), (cr, cc + 1), (cr, cc - 1)):
if 0 <= nr < rows and 0 <= nc < cols and grid[nr][nc] == 1 and not seen[nr][nc]:
seen[nr][nc] = True
queue.append((nr, nc))
sizes.append(size)
return sizes
def count_outages(grid):
return len(outage_sizes(grid))
def largest_outage(grid):
return max(outage_sizes(grid), default=0)
def outages_after_each(events):
parent = {}
size = {}
def find(x):
while parent[x] != x:
parent[x] = parent[parent[x]]
x = parent[x]
return x
count = 0
result = []
for r, c in events:
block = (r, c)
if block not in parent:
parent[block] = block
size[block] = 1
count += 1
for neighbor in ((r + 1, c), (r - 1, c), (r, c + 1), (r, c - 1)):
if neighbor in parent:
a, b = find(block), find(neighbor)
if a != b:
if size[a] < size[b]:
a, b = b, a
parent[b] = a
size[a] += size[b]
count -= 1
result.append(count)
return result
Complexity: Parts 1 and 2: O(rows x cols). Part 3: nearly O(1) amortized per event with union by size and path halving.
What the interviewer will probe: A grid with no outages, one outage covering the whole grid, a single row or column, the same block reported twice, and one event that merges three or four outages.
Problem 7MediumTopological sort
Package install order
def install_order(deps), def install_waves(deps)
A package manager must install dependencies before the packages that need them. deps maps each package to the list of packages it depends on, and every package that appears anywhere must be installed.
Part 1. Write install_order(deps), which returns any valid install order.
Part 2. Now detect circular dependencies: raise ValueError instead of returning an order.
Part 3. Now write install_waves(deps). Wave 1 holds everything with no dependencies, wave 2 everything whose dependencies are all in earlier waves, and so on. Sort the names inside each wave.
Ask whether a package can list the same dependency twice. Counting a set of dependencies keeps the counts right.
Kahn's algorithm finds cycles for free: packages that never reach zero waiting dependencies are on or behind a cycle.
Waves show what can install in parallel, which is what a real installer wants.
Approach
Collect every package, count how many distinct dependencies each one is waiting on, and map each dependency to the packages that use it. Start with the packages waiting on nothing; installing a wave lowers the counts of their users, and users that reach zero form the next wave. If fewer packages get installed than exist, there is a cycle.
Python 3 solution
def install_waves(deps):
packages = set(deps)
for needed in deps.values():
packages.update(needed)
waiting = {pkg: len(set(deps.get(pkg, []))) for pkg in packages}
users = {pkg: [] for pkg in packages}
for pkg, needed in deps.items():
for dep in set(needed):
users[dep].append(pkg)
wave = sorted(pkg for pkg in packages if waiting[pkg] == 0)
waves = []
installed = 0
while wave:
waves.append(wave)
installed += len(wave)
ready = []
for pkg in wave:
for user in users[pkg]:
waiting[user] -= 1
if waiting[user] == 0:
ready.append(user)
wave = sorted(ready)
if installed != len(packages):
raise ValueError('circular dependency')
return waves
def install_order(deps):
return [pkg for wave in install_waves(deps) for pkg in wave]
Complexity: O(P log P + D) time for P packages and D dependency links (the log comes from sorting waves), O(P + D) space.
What the interviewer will probe: A package that appears only as a dependency, duplicate dependencies, a package that depends on itself, a cycle away from the first package, and an empty dict.
Problem 8HardGraph evaluation + cycle detection
Spreadsheet with formulas
class Sheet: set_cell(cell, value), get_cell(cell)
Build a tiny spreadsheet. Cell names look like "A1" or "C12".
Part 1. Support set_cell(cell, value) for integers and get_cell(cell). An empty cell reads as 0.
Part 2. Now a value can be a formula string like "=A1+B2+7": cell names and integers joined by plus signs. get_cell evaluates it, and formulas can point at other formulas.
Part 3. Now detect cycles: if a formula depends on itself, directly or through other cells, get_cell raises ValueError. Then talk through how you would avoid re-evaluating every formula on every read.
Track only the cells on the current evaluation path, not every cell ever seen, or a diamond (two formulas sharing one input) looks like a cycle.
For speed, cache each formula's value and keep a reverse map of dependents, so set_cell clears only the cells downstream of the change.
Recursion depth grows with the length of a formula chain; mention an explicit stack for very long chains.
Approach
Store raw values. get_cell evaluates recursively: a plain number returns as is, and a formula sums its terms, evaluating cell names along the way. A set of the cells on the current path catches cycles: meeting a cell that is already on the path means the formula loops back on itself. The cell leaves the set when its evaluation finishes.
Python 3 solution
class Sheet:
def __init__(self):
self.cells = {}
def set_cell(self, cell, value):
self.cells[cell] = value
def get_cell(self, cell):
return self._eval(cell, set())
def _eval(self, cell, path):
value = self.cells.get(cell, 0)
if not (isinstance(value, str) and value.startswith('=')):
return value
if cell in path:
raise ValueError(f'cycle at {cell}')
path.add(cell)
total = 0
for term in value[1:].split('+'):
term = term.strip()
if term.lstrip('-').isdigit():
total += int(term)
else:
total += self._eval(term, path)
path.remove(cell)
return total
Complexity: Without caching, get_cell is O(size of the formula graph reachable from the cell) per call.
What the interviewer will probe: Empty cells, a diamond dependency (not a cycle), a self-reference like A1 = "=A1", a long chain of formulas, and overwriting a formula with a plain number.
How to practice for the CoderPad round
Step 1
Use the CoderPad sandbox
CoderPad's free sandbox is nearly identical to a real pad, minus the multi-user features, AI assist and saving. Paste a problem from this page into it and solve it there.
Step 2
Talk the whole time
Restate the problem, ask one clarifying question, give your plan, then code. Read the talking points under each problem: those are the things interviewers like to hear.
Step 3
Run Part 1 before the follow-up
Get the first part working and run it on the example before you start Part 2. A clean, tested base makes each follow-up a small change instead of a rewrite.
Step 4
Practice with a friend as the interviewer
Have someone read the parts to you one at a time, without showing you what comes next. That is the closest thing to a real live pad.
OphyAI Desktop for Mac and Windows
Practice with these, then use Coding Copilot in your CoderPad interview
Coding Copilot in the OphyAI desktop app reads the coding problem on your screen with one hotkey and returns an approach, complexity notes and working code in your language.
Practice with these, then bring Coding Copilot into the real CoderPad interview. While you practice, press Cmd/Ctrl + Shift + A to snap the problem window, or Cmd/Ctrl + Shift + S to drag a box around one problem, and Coding Copilot returns an answer card: how it read the problem, what to say first, the code with line numbers, a dry run with edge cases, and the Big O. Compare it with your own attempt.
In a live CoderPad interview, press Space or Cmd/Ctrl + Shift + L and it transcribes the follow-up your interviewer asks out loud. Follow-ups remember the problem, so "now handle malformed lines" updates the same code instead of starting over. The card shows in a floating overlay that macOS and Windows leave out of screen sharing and recording. It stays on top of a full-screen editor or browser and opens without taking focus from your editor.
Cmd/Ctrl + Shift + A snaps the problem window, even when other windows cover it, and Cmd/Ctrl + Shift + S lets you drag a box around part of the screen. In a live interview, Space or Cmd/Ctrl + Shift + L transcribes the question the interviewer asks out loud.
Answers show as a card in a floating overlay that macOS and Windows leave out of screen sharing and recording. It stays on top of a full-screen editor or browser and opens without taking the keyboard from your editor.
Each answer card shows how OphyAI read the problem, what to say first, the code with line numbers, a dry run with edge cases, and the Big O. Follow-ups remember the problem, so "now handle duplicates" updates the same code.
Free download. Coding Interview is on the Premium plan ($59 a month, 500 credits) and uses 15 credits per session. See everything it does
What it is hidden from, and what it is not
Every keystroke in the pad is recorded for playback, and pastes and leaving the tab are flagged. Companies can turn on CoderPad's own AI assist, and every prompt is saved for them to review.
The OphyAI window is hidden from screen sharing and recording, but not from a webcam, a person watching, or the platform's own keystroke, paste and tab tracking.
CoderPad interview questions FAQ
No. CoderPad says each company writes its own interview questions, so it has no practice question bank. The problems here are original and built as a base problem plus follow-ups, because your interviewer may add follow-ups as you go.
CoderPad's free sandbox at app.coderpad.io/sandbox is nearly identical to a real pad, minus the multi-user features, AI assist and saving. Paste one of the problems from this page into it and solve it out loud.
The one you are most fluent in. CoderPad supports dozens of languages, and some questions let you switch. Python keeps code short, which leaves more time for talking through your plan.
Usually. There is a Run button and, for some languages, a REPL. Some companies turn code execution off, and you will see "Execution Disabled" in the top left corner if they have.
Set a timer, read the problem out loud, ask one clarifying question, give a plan in two sentences, then code Part 1 and run it on the example. Only then read the next part. The talking points under each problem give you things worth saying.
Coding Copilot in the OphyAI desktop app is built for live pads: it transcribes the question your interviewer asks, reads the problem on screen with one hotkey, and shows an approach and working code in an overlay hidden from screen sharing. It does not hide from a webcam or from CoderPad's keystroke playback and paste and tab flags.
Sources
Format facts read on November 3, 2026. Every problem on this page is original and written by OphyAI for practice; none is a real CoderPad question. OphyAI is not affiliated with CoderPad.
Practice here, then take Coding Copilot into your CoderPad interview
Download OphyAI Desktop and bring Coding Copilot into your next coding interview: it hears the question, reads the problem, and shows the approach and code in an overlay your screen share leaves out.