Skip to main content
Practice questions · Checked November 3, 2026

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.

What a live CoderPad interview looks like

  • 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.

#ProblemDifficultyPattern
1Access log summaryEasyParsing + hash maps
2API rate limiterMediumSliding window + deque
3Text editor with undo and redoMediumTwo stacks
4Flatten and unflatten a configMediumRecursion on nested data
5Team calendar free slotsMediumInterval merging
6Power outage mapHardBFS, then union-find
7Package install orderMediumTopological sort
8Spreadsheet with formulasHardGraph evaluation + cycle detection
Problem 1EasyParsing + hash maps

Access log summary

def count_by_path(lines), def top_paths(lines, k), def p95_by_path(lines)

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.

  1. Part 1. Write count_by_path(lines), which returns a dict from each path to the number of requests it received.
  2. Part 2. Now write top_paths(lines, k), which returns the k paths with the most requests. Break ties alphabetically by path.
  3. 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.

Example

Input
lines = ["GET /home 200 120", "GET /api/users 200 35",
         "POST /api/users 201 80", "GET /home 500 900",
         "bad line", "GET /home 200 abc"]
Output
count_by_path(lines) -> {"/home": 2, "/api/users": 2}
top_paths(lines, 1) -> ["/api/users"]
p95_by_path(lines)  -> {"/home": 900, "/api/users": 80}
Why
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.

  1. Part 1. Implement allow(user, t), which returns True and records the request if the user is under the limit, otherwise False.
  2. Part 2. Now the service has millions of users. Make sure users who have gone quiet do not hold memory forever.
  3. 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.

Example

Input
limiter = RateLimiter(2, 10)
limiter.allow("ana", 1)
limiter.allow("ana", 5)
limiter.allow("ana", 9)
limiter.allow("ana", 11)
limiter.retry_after("ana", 12)
Output
True
True
False
True
3
Why
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.

  1. 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.
  2. 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.
  3. Part 3. Now add redo(), which re-applies the most recently undone change. Any new append or delete clears the redo history.

Example

Input
e = Editor()
e.append("hello"); e.append(" world"); e.delete(6)
e.text()
e.undo(); e.text()
e.undo(); e.text()
e.redo(); e.text()
e.append("!"); e.redo(); e.text()
Output
"hello"
"hello world"
"hello"
"hello world"
"hello world!"
Why
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.

Python 3 solution
class Editor:
    def __init__(self):
        self.chars = []
        self.undo_stack = []
        self.redo_stack = []

    def _apply(self, kind, payload):
        if kind == 'append':
            self.chars.extend(payload)
        else:
            del self.chars[len(self.chars) - len(payload):]

    def append(self, text):
        self._apply('append', text)
        self.undo_stack.append(('append', text))
        self.redo_stack.clear()

    def delete(self, k):
        removed = ''.join(self.chars[max(0, len(self.chars) - k):])
        self._apply('delete', removed)
        self.undo_stack.append(('delete', removed))
        self.redo_stack.clear()

    def undo(self):
        if not self.undo_stack:
            return
        kind, payload = self.undo_stack.pop()
        self._apply('delete' if kind == 'append' else 'append', payload)
        self.redo_stack.append((kind, payload))

    def redo(self):
        if not self.redo_stack:
            return
        kind, payload = self.redo_stack.pop()
        self._apply(kind, payload)
        self.undo_stack.append((kind, payload))

    def text(self):
        return ''.join(self.chars)

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.

  1. Part 1. Write flatten(config) for nested dicts: {"db": {"host": "x", "port": 5432}} becomes {"db.host": "x", "db.port": 5432}.
  2. 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.
  3. Part 3. Now write unflatten(flat), which rebuilds the original. A parent whose child keys are exactly 0, 1, 2 ... becomes a list.

Example

Input
config = {"db": {"host": "x", "ports": [5432, 5433]}, "debug": False}
flat = flatten(config)
unflatten(flat) == config
Output
flat -> {"db.host": "x", "db.ports.0": 5432,
         "db.ports.1": 5433, "debug": False}
True
Show approach and solution

Say this out loud

  • 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.

Problem 5MediumInterval merging

Team calendar free slots

def merge(busy), def free_slots(calendars, day_start, day_end, duration=0)

Times are minutes after midnight. Each person's calendar is a list of busy meetings [start, end), which may overlap and are not sorted.

  1. Part 1. Write merge(busy), which merges overlapping or touching meetings into a sorted list: [[60, 90], [80, 120], [120, 150]] becomes [[60, 150]].
  2. Part 2. Now write free_slots(calendars, day_start, day_end), which returns every gap inside [day_start, day_end) when nobody is busy.
  3. Part 3. Now take a duration and return only the gaps that are at least that many minutes long.

Example

Input
calendars = [[[540, 600], [720, 780]], [[570, 660]], [[900, 960]]]
free_slots(calendars, 480, 1020)
free_slots(calendars, 480, 1020, duration=90)
Output
[[480, 540], [660, 720], [780, 900], [960, 1020]]
[[780, 900]]
Show approach and solution

Say this out loud

  • 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.

Problem 6HardBFS, then union-find

Power outage map

def count_outages(grid), def largest_outage(grid), def outages_after_each(events)

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.

  1. Part 1. Write count_outages(grid), which returns how many separate outages there are.
  2. Part 2. Now write largest_outage(grid), which returns the size of the largest outage in blocks (0 if there is none).
  3. 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.

Examples

Input
grid = [[1, 1, 0, 0],
        [0, 1, 0, 1],
        [1, 0, 0, 1]]
count_outages(grid)
largest_outage(grid)
Output
3
3
Input
outages_after_each([[0, 0], [0, 2], [0, 1], [0, 1], [1, 1]])
Output
[1, 2, 1, 1, 1]
Why
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.

  1. Part 1. Write install_order(deps), which returns any valid install order.
  2. Part 2. Now detect circular dependencies: raise ValueError instead of returning an order.
  3. 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.

Examples

Input
deps = {"app": ["web", "db"], "web": ["http"], "db": [], "http": []}
install_waves(deps)
install_order(deps)
Output
[["db", "http"], ["web"], ["app"]]
["db", "http", "web", "app"]
Input
install_order({"a": ["b"], "b": ["a"]})
Output
ValueError
Show approach and solution

Say this out loud

  • 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".

  1. Part 1. Support set_cell(cell, value) for integers and get_cell(cell). An empty cell reads as 0.
  2. 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.
  3. 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.

Example

Input
s = Sheet()
s.set_cell("A1", 5)
s.set_cell("B1", "=A1+10")
s.set_cell("C1", "=A1+B1+1")
s.get_cell("C1")
s.get_cell("D9")
s.set_cell("A1", "=C1")
s.get_cell("C1")
Output
21
0
ValueError (A1 -> C1 -> A1)
Show approach and solution

Say this out loud

  • 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

  1. 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.

  2. 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.

  3. 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.

  4. 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.

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.