Programming Fundamentals and Code Quality Questions
Encompasses core programming skills, data structures, basic algorithms, language fundamentals, and code quality practices. Expect proficiency with arrays, strings, lists, hash maps or dictionaries, sets, common collection operations, basic sorting and searching algorithms, and tradeoffs between data structures. Understand control flow, functions and modular design, classes and object oriented programming concepts including encapsulation, inheritance, and polymorphism, exception handling, file input and output, and common language idioms for mainstream interview languages such as Python, Java, and C plus plus. Emphasizes writing clean, readable, maintainable code: meaningful naming, modular functions, small interfaces, handling edge cases and errors, logging and documentation, simple testing and debugging strategies, and awareness of time and space complexity for common operations. Candidates should be able to implement correct solutions, follow language specific idioms where appropriate, and demonstrate attention to code quality and readability.
EasyTechnical
26 practiced
Implement `binary_search(arr: List[int], target: int) -> int` in Python that returns the index of target in a sorted ascending array or -1 if not found. Provide an iterative implementation, explain boundary conditions (duplicates) and off-by-one pitfalls, and state time and space complexity.
Sample Answer
Use a classic iterative binary search: maintain inclusive left and right indices, compute mid as left + (right-left)//2 to avoid overflow, compare arr[mid] to target and move bounds accordingly. For duplicates this returns the index of one matching element (not necessarily first/last); to find first/last occurrence you adapt the bound update. Watch off-by-one: ensure when arr[mid] < target you set left = mid + 1 (not mid), and when arr[mid] > target set right = mid - 1.
python
from typing import List
def binary_search(arr: List[int], target: int) -> int:
"""
Iterative binary search on sorted ascending array.
Returns index of target or -1 if not found.
"""
left, right = 0, len(arr) - 1
while left <= right: # inclusive bounds
mid = left + (right - left) // 2
if arr[mid] == target:
return mid
elif arr[mid] < target:
left = mid + 1 # move past mid
else:
right = mid - 1 # move before mid
return -1
Key points:- Duplicates: this returns any matching index. To get first occurrence, on arr[mid] == target set right = mid - 1 and record mid; for last occurrence set left = mid + 1 and record mid.- Off-by-one pitfalls: using left < right loop or wrong mid update can skip elements or loop infinitely; inclusive bounds with left <= right and +1/-1 updates avoid that.Time Complexity: O(log n). Space Complexity: O(1).
HardSystem Design
24 practiced
Design a robust checkpointing strategy for long-running distributed training jobs. Cover atomic checkpoint writes, consistency across workers, handling partial failures, metadata/versioning, checkpoint frequency vs overhead, and how to resume training after node failures minimizing lost work.
Sample Answer
Requirements & constraints:- Durable, atomic checkpoints across N workers (millions of params, sharded).- Minimize paused training time and lost work on failure.- Work with object stores (S3/GCS) and shared filesystems.- Support partial failures, versioning, and GC.High-level design:- Coordinator (master/driver) orchestrates periodic global checkpoints. Workers produce local shard files and a small metadata manifest. All writes go to durable object storage. Coordinator enforces a barrier: checkpoint epoch k begins → workers snapshot local state (optimizer, RNG, iteration) → write shard to temp path → report checksum and size to coordinator → coordinator validates all reports → commit metadata manifest (atomic).Atomic checkpoint writes and consistency:- Workers write to temp/object-prefix: /checkpoints/epoch_k/worker_i.part. Use multipart or write-then-rename semantics. Since object stores lack atomic rename, use two-step commit: upload parts, then write manifest("/checkpoints/epoch_k/MANIFEST.json") containing list of shards, checksums, sizes, epoch, timestamp, training step, upstream commit hash. The manifest is the authoritative atomic pointer; readers only consider checkpoints with a complete manifest and valid checksums.- Use strong checksums (SHA256) and file sizes to detect partial uploads.Handling partial failures:- If a worker fails during upload: coordinator waits bounded timeout, then either (a) retry with same worker on new node (resuming multipart upload) or (b) mark epoch incomplete and fall back to previous committed checkpoint. Keep in-progress markers (/inprogress/epoch_k) with leases; coordinator renews lease during barrier.- Support resumable uploads (multipart) so large shards can continue after transient network issues.Metadata & versioning:- Manifest schema: {epoch, global_step, optimizer_state_descriptor, shard_list:[{worker_id, path, checksum, size}], dependencies:[prev_epoch], framework_version, code_git_hash, hyperparams_hash}.- Use monotonic epoch numbers and include framework/code hashes to ensure compatibility on resume. Store manifests in a versioned index (e.g., /checkpoints/index.json) or store manifests with timestamped names and maintain a symlink/latest_manifest in metadata service (or write latest pointer atomically via object store conditional write).Checkpoint frequency vs overhead:- Trade-offs: frequent checkpoints reduce lost work but increase I/O and training pause. Strategies: - Fixed interval + adaptive: checkpoint every T minutes or every M steps, with T chosen to keep expected lost work <= acceptable bound based on failure rate λ. Use formula: optimal interval ≈ sqrt(2 * checkpoint_cost / λ). - Incremental/differential checkpoints: only upload deltas for parameters that changed significantly (sparse) or use chunked content-addressable storage to dedupe unchanged chunks. - Asynchronous or nonblocking checkpointing: use background threads to copy GPU->CPU and upload while training continues; ensure consistency by snapshotting via copy-on-write or checkpoint-local queues. For synchronous SGD, still need a global barrier to snapshot consistent optimizer/step; allow worker-local parameter saves asynchronously but save global barrier metadata synchronously (small).Minimizing lost work & resume procedure:- Recovery coordinator picks highest-valid manifest (highest epoch) and instructs workers to restore shards. If a worker was mid-epoch when failed, two options: - Restore to last committed epoch and re-run from there (simple). - Use incremental checkpoints + replay logs: keep per-worker operation logs or RNG seeds to deterministically replay updates since last commit (complex).- To minimize wasted steps, keep lightweight frequent metadata-only checkpoints (global_step, RNG, small optimizer summaries) and less-frequent full snapshots. On resume, restore full snapshot and fast-forward using saved micro-checkpoints or buffered gradients if available.- For spot/preemptible nodes: maintain redundant shard copies (replication factor R=2) or use erasure coding for storage efficiency and faster restores.Scalability & performance optimizations:- Shard checkpoints per worker to parallelize uploads.- Use multipart uploads and concurrent clients; throttle to avoid overwhelming storage.- Deduplicate via content-addressable chunking; compress shards; use sparse tensor serialization.- Use checkpoint push/pull proxies or local caching tiers (fast SSD cache or checkpoint gateway) to absorb bursts.Trade-offs and rationale:- Two-phase commit with manifest gives simple atomicity for object stores.- Synchronous barrier ensures consistency but costs pause — mitigated by background uploads and minimal synchronous metadata commit.- Incremental and asynchronous strategies reduce overhead but add complexity for correctness (must ensure determinism for replay).Operational considerations:- Test restore regularly; validate manifests and checksums automatically.- Garbage collection: coordinator keeps last K checkpoints, deletes older manifests and shards after verifying newer checkpoint integrity. Use tombstones and delayed deletion to handle eventual consistency.- Monitor metrics: checkpoint latency, upload throughput, failure/retry rates, and recovery time objective (RTO).This strategy balances atomicity and consistency with practical performance: authoritative manifests for atomic commits, resumable multipart uploads for robustness, hybrid checkpoint frequency (metadata vs full) to minimize lost work, and a clear recovery protocol that resumes from the latest validated checkpoint.
MediumTechnical
33 practiced
Given a long, single-purpose Python function that mixes data loading, preprocessing, augmentation, and training steps, explain how you'd refactor it into modular, testable components. Give concrete suggestions for function/class boundaries, interfaces, dependency injection, and where to add unit and integration tests.
Sample Answer
Approach summary: break the monolith into clear layers with single responsibilities (I/O, preprocessing, augmentation, model, training loop, config). Use small pure functions and classes with explicit interfaces and dependency injection so each piece can be unit-tested and swapped in for experiments.Concrete boundaries and interfaces- Data I/O: class FileDataset/TFRecordDataset/PandasLoader - Responsibility: read raw records, yield raw examples - Interface: __len__(), __getitem__(idx) or iterator() for streaming- Preprocessing: pure functions or transformer objects (implement transform(example) -> example) - Use dataclasses + typing for schema - Compose transforms (Compose([Normalize(), Tokenize(), ToTensor()]))- Augmentation: separate Transform classes; deterministic vs stochastic injected via rng seed- Model & Trainer: Trainer class accepts model, optimizer, loss_fn, dataloaders, callbacks - Trainer.train(epochs, checkpoint_callback) - Inject scheduler, mixed-precision flag, device- Config: use a small config object or pydantic dataclass; avoid global varsExample interface (Python):
python
from typing import Protocol, Any
class Preprocessor(Protocol):
def transform(self, record: dict) -> dict: ...
class Trainer:
def __init__(self, model, optimizer, loss_fn, train_loader, val_loader, device='cpu'): ...
def train(self, epochs:int): ...
Dependency injection- Pass dataset/preprocessor/augmenter into DataLoader or Trainer constructor.- Use factory functions in tests to provide small synthetic datasets.- Use environment/config injection for seeds, devices, and distributed settings.Testing strategy- Unit tests: - Pure preprocessors/augmenters: fast, deterministic inputs -> expected outputs - File I/O: mock filesystem or use tmpdir fixtures; test that loader parses schema - Trainer utilities (metric computations, checkpoint naming) in isolation- Integration tests: - Small end-to-end on synthetic data to verify train loop runs, gradients update, and metrics change - Use parametrized tests for CPU/GPU toggles where applicable- Use mocks: - Mock heavy dependencies (cloud storage, DB) to test orchestration without network- CI considerations: - Keep unit tests <1s each; run integration tests nightly or in a separate pipeline - Use coverage to ensure critical paths testedOther best practices- Add type hints and docstrings for all public methods- Centralize logging and metrics emission for observability- Keep randomness controllable via injected RNG or torch.manual_seed for reproducible tests- Provide small examples / smoke tests to validate deployable artifactsThis refactor yields modular, testable units, faster debugging, and easier experimentation (swap preprocessors, augmenters, models) while enabling robust unit and integration testing.
MediumTechnical
27 practiced
Given `nums: List[int]` and integer `k`, implement `top_k_frequent(nums, k)` in Python that returns the k most frequent elements. Aim for O(n log k) or better. Explain trade-offs between using a heap and bucket sort, and discuss handling ties.
Sample Answer
Approach: count frequencies with a hash map, then either (A) use a min-heap of size k to keep k highest-frequency items (O(n log k) time), or (B) use bucket sort (frequency-indexed lists) for O(n) time when max frequency is not huge. Below is a heap-based implementation (recommended when k << n and memory for buckets could be large).
python
from collections import Counter
import heapq
from typing import List
def top_k_frequent(nums: List[int], k: int) -> List[int]:
# Count frequencies: O(n)
freq = Counter(nums)
# Min-heap of (frequency, num). Keep size k -> O(n log k)
heap = []
for num, f in freq.items():
if len(heap) < k:
heapq.heappush(heap, (f, num))
else:
# If current frequency larger than smallest in heap, replace
if f > heap[0][0]:
heapq.heapreplace(heap, (f, num))
# Extract results (order not guaranteed by frequency descending)
return [num for (_, num) in heapq.nlargest(k, heap)]
Key points:- Time: O(n log k) for heap approach; O(n) average for bucket sort (plus O(n) space for buckets).- Space: O(n) for counts; heap uses O(k); buckets use O(n) in worst case (max freq ≤ n).- Trade-offs: - Heap: better when k is small relative to distinct elements; streamable; predictable memory O(k). - Bucket sort: optimal linear time, simple to implement, but requires building list of size max_freq+1 and iterating; may be less memory-efficient if max_freq large.- Handling ties: problem typically allows any order among tied frequencies. If deterministic tie-breaking needed, include secondary key (e.g., value) when pushing to heap or collect and sort ties by value.
HardTechnical
33 practiced
You manage several ML teams with a growing codebase and mounting technical debt. Propose a staged plan to enforce code quality: linters/formatters, type hints, pre-commit hooks, unit/integration tests, CI gating, code review standards, and measurable ways to reduce debt without stalling product delivery.
Sample Answer
Situation: Our ML org has a growing codebase, mounting technical debt, and teams shipping features quickly but with increasing fragility.Plan (staged, 6 months with ongoing governance):1. Assess (2–3 weeks)- Inventory repos, test coverage, lint errors, flaky tests, deployment incidents.- Score each repo (risk, change velocity, owner) to prioritize efforts.2. Quick wins — developer experience (weeks 1–6)- Standardize formatter + linter (black/ruff or clang-format + flake8) and a shared config.- Add pre-commit hooks to run fast checks locally (format, basic lint, safety scan).- Outcome metric: reduce CI lint failures by 80% within 1 month of rollout.3. Type hints and incremental typing (weeks 4–12)- Introduce typing policy: new modules and modified files must include type hints. Use mypy in “warn” mode initially.- Start with public APIs and model interfaces.- Measure: % of modified files with types; aim for 50% after 3 months, 90% in 12 months.4. Tests (ongoing, prioritize high-risk)- Require unit tests for new code and for bug fixes; add integration tests for model training/serving pipelines.- Introduce test templates and lightweight fixtures to reduce author burden.- Metric: test coverage on critical modules >= 70%; reduce production rollbacks by X%.5. CI gating and progressive enforcement (weeks 6+)- CI runs lints, type checks, unit tests; fail-on-critical but allow warnings for legacy files.- Use branch protection: passing CI + two approvals for main branches.- Gradually tighten gates per repo based on risk score.6. Code review standards & education (ongoing)- Publish checklist: readability, reproducibility (fixed seeds, dataset hashes), performance/regression risks, test coverage.- Hold weekly review office-hours and monthly brown-bags on testing, typing, and ML-specific pitfalls.- Track PR metrics: time-to-review, rework rate, and approval size.7. Reduce technical debt without blocking features- Create a “debt backlog” with estimated effort and ROI; allocate a steady cadence (e.g., 10–20% sprint capacity or one “debt” sprint every 6 weeks).- Use "ship + follow-up" tactic: allow small feature PRs with a mandatory linked debt ticket to refactor later.- Reward debt reduction in performance reviews and team KPIs.Measurable KPIs (track weekly/monthly)- Lint violations / repo- Mypy strictness score or typed-lines %- Test coverage (critical modules)- CI pass rate, deployment failures- Mean time to revert / rollback- Percentage of sprint capacity on debt tasksTrade-offs and governance- Accept temporary slower velocity; mitigate by limiting initial scope, automating checks, and pairing debt work with feature delivery.- Appoint a quality guild and rotating repo maintainers to sustain standards.Result expectation: within 3 months, visible reduction in CI noise and faster, safer merges; within 6–12 months, measurable drop in incidents and a sustainable cadence for continued debt reduction without halting feature delivery.
Unlock Full Question Bank
Get access to hundreds of Programming Fundamentals and Code Quality interview questions and detailed answers.