Python Coding and Data Structures Questions
Proficiency in Python, including arrays, dictionaries, linked lists, and basic algorithms. Ability to write efficient, clean code under time pressure. Understanding of time/space complexity and optimization.
HardTechnical
45 practiced
Implement a thread-safe bounded FIFO queue in Python without using queue.Queue. Use threading primitives (Lock, Condition) to implement put(item) and get() with blocking behavior when full/empty. Explain how this pattern is useful in producer-consumer data-loaders for training.
Sample Answer
Approach: use a fixed-size collections.deque as the buffer, a Lock to protect state, and two Condition variables (not_full, not_empty) bound to the same Lock. put() blocks when buffer is full until space available; get() blocks when empty until an item exists. Conditions handle spurious wakeups by checking the predicate in a loop.Key points and reasoning:- Use Condition objects tied to the same Lock so state checks and waits happen atomically.- Always check predicates in a loop to handle spurious wakeups.- notify() wakes a single waiter (efficient); use notify_all() if multiple consumers must be awakened.- Operations are O(1) amortized (deque append/popleft). Space is O(maxsize).Usefulness for ML producer-consumer data-loaders:- Decouples slow producers (disk/augmentation) and fast consumers (GPU training) to keep GPUs fed without over-consuming memory.- Bounded buffer enforces memory limits and backpressure: producers block when buffer full, avoiding OOM.- Thread-safety lets multiple loader threads produce batches concurrently while training thread(s) consume them safely.- Conditions provide efficient blocking (no busy-wait), reducing CPU usage and increasing throughput—critical for high-performance training pipelines.Edge cases:- Interrupted threads or shutdown: add non-blocking variants or timeout parameters and a sentinel to signal termination.- If multiple producers/consumers exist, prefer notify_all when complex wake patterns occur.
python
import threading
from collections import deque
from typing import Any
class BoundedQueue:
def __init__(self, maxsize: int):
if maxsize <= 0:
raise ValueError("maxsize must be > 0")
self.maxsize = maxsize
self.buffer = deque()
self.lock = threading.Lock()
self.not_empty = threading.Condition(self.lock)
self.not_full = threading.Condition(self.lock)
def put(self, item: Any):
"""Block if full, then append item."""
with self.not_full:
while len(self.buffer) >= self.maxsize: # predicate loop for spurious wakeups
self.not_full.wait()
self.buffer.append(item)
# Notify one waiter on not_empty that an item is available
self.not_empty.notify()
def get(self) -> Any:
"""Block if empty, then pop left and return item."""
with self.not_empty:
while len(self.buffer) == 0:
self.not_empty.wait()
item = self.buffer.popleft()
# Notify one waiter on not_full that space is available
self.not_full.notify()
return item
def qsize(self) -> int:
with self.lock:
return len(self.buffer)
def empty(self) -> bool:
with self.lock:
return len(self.buffer) == 0
def full(self) -> bool:
with self.lock:
return len(self.buffer) >= self.maxsizeEasyTechnical
44 practiced
Explain the difference between Python 3 bytes and str. When reading data files for ML (text logs, CSV, JSON), what pitfalls exist with encodings and how would you robustly read files that may contain mixed encodings? Mention defaults and common flags in open() and possible preprocessing steps.
Sample Answer
Bytes vs str:- bytes is raw 8-bit data (immutable sequence of bytes). str is a sequence of Unicode code points (text). In Python 3 you must decode bytes → str (bytes.decode(encoding)) and encode str → bytes (str.encode(encoding)). Many I/O operations return str when you open file in text mode ('r') and bytes in binary mode ('rb').Pitfalls when reading ML data:- Files may use different encodings (UTF-8, UTF-8 with BOM, Windows-1252, ISO-8859-1, etc.). Mixed encodings inside a file, invalid byte sequences, BOMs, and null bytes can break decoders. JSON requires valid Unicode; CSV/text logs often have inconsistent encodings or stray non-printable bytes. Defaults: open(..., mode='r') uses locale.getpreferredencoding(False) (often 'utf-8' on modern systems but not guaranteed). pandas functions also default to 'utf-8' unless specified.Robust approach / practical steps:1. Prefer binary first for detection: open(path, 'rb') and inspect initial bytes for BOM or sample bytes.2. Use an encoding detector (chardet or charset_normalizer) on a representative sample if encoding unknown.3. Try decoding in order: 'utf-8-sig' (handles BOM) → 'utf-8' → detector-suggested encoding → 'latin-1' / 'windows-1252' as fallback. latin-1 will never fail and preserves bytes to characters 0–255.4. Use errors='surrogateescape' when opening in text mode to preserve undecodable bytes for later handling, or errors='replace' to mark problems.5. For mixed encodings or problematic files, read as bytes and decode per-field/column or normalize bytes before parsing.6. Normalize line endings (.replace(b'\r\n', b'\n')) and strip embedded nulls if parser cannot handle them.Common open() flags:- encoding='utf-8' / 'utf-8-sig' / 'latin-1'- errors='strict' (default) / 'replace' / 'ignore' / 'surrogateescape'- newline='' for csv module to handle newlines properly- mode='rb' for binary-safe detectionExample pattern:For CSV/JSON and pandas:- Pass encoding and engine explicitly: pandas.read_csv(..., encoding='utf-8', engine='python') or try engine='python' if C engine fails.- For JSON lines read in bytes and decode per-line using above strategy.Key trade-offs:- 'latin-1' preserves bytes but may misinterpret characters semantically.- errors='surrogateescape' preserves original bytes for lossless round-trip but requires later handling.In ML pipelines, automate detection + consistent normalization (convert everything to UTF-8 str, remove/control unknown chars) and log files that failed decoding for manual review.
python
import chardet
def robust_read_text(path, sample_size=8192):
with open(path, 'rb') as f:
raw = f.read(sample_size)
if raw.startswith(b'\xef\xbb\xbf'):
enc = 'utf-8-sig'
else:
guess = chardet.detect(raw)
enc = guess['encoding'] or 'latin-1'
try:
with open(path, 'r', encoding=enc, errors='strict', newline='') as f:
return f.read()
except UnicodeDecodeError:
# fallback to safe decode preserving problematic bytes
with open(path, 'r', encoding='utf-8', errors='surrogateescape') as f:
return f.read()HardTechnical
29 practiced
Implement a consistent hashing ring in Python for distributing keys to N nodes with minimal remapping when nodes are added/removed. Support virtual nodes for balancing. Provide code for add_node(node_id), remove_node(node_id), and get_node(key). Explain time complexity and how you'd use this to shard a feature store or embedding cache.
Sample Answer
Approach: Use a sorted list of hash positions (the ring) mapping to node IDs. Each physical node is represented by V virtual nodes (tokens) — hashed as node_id + replica_index — to improve balance. get_node(key) hashes the key, finds the first token >= key hash with bisect (wraps to start), and returns its physical node. Adding/removing a node only affects its virtual tokens => minimal remapping.Key points:- Time complexity: add_node/remove_node O(R * log M) where R = replicas, M = total tokens; get_node O(log M) using binary search.- Space: O(M) tokens stored.- Minimal remapping: When a node is added/removed only keys mapping to that node’s token ranges move (~proportional to replicas/total_nodes).- Improvements: use 64-bit hash, consistent hashing with replication (store multiple distinct nodes per key) for fault tolerance, virtual node count tuning based on expected skew.Using for sharding a feature store or embedding cache:- Hash feature or embedding key to determine shard (node) for storage and retrieval; this uniformly distributes records across cache instances.- When scaling up/down the number of cache/feature-store shards, only a fraction of keys are remapped, reducing mass data migration and cache cold starts.- For embeddings: combine consistent hashing with replication and a background rebalancer to asynchronously move hot embeddings to new nodes and update routing metadata (e.g., in a centralized config or service discovery).
python
import bisect
import hashlib
from typing import Dict, List, Tuple
def _hash(value: str) -> int:
# 32-bit unsigned integer hash
return int(hashlib.md5(value.encode()).hexdigest()[:8], 16)
class ConsistentHashRing:
def __init__(self, replicas: int = 100):
self.replicas = replicas
self._ring: List[int] = [] # sorted hashes (tokens)
self._ring_map: Dict[int, str] = {} # token_hash -> node_id
def add_node(self, node_id: str):
"""Add physical node with 'replicas' virtual nodes."""
for i in range(self.replicas):
token = _hash(f"{node_id}:{i}")
if token in self._ring_map:
# rare collision; skip or continue probing
continue
bisect.insort(self._ring, token)
self._ring_map[token] = node_id
def remove_node(self, node_id: str):
"""Remove a node and all its virtual nodes."""
to_remove = []
for i in range(self.replicas):
token = _hash(f"{node_id}:{i}")
if token in self._ring_map and self._ring_map[token] == node_id:
to_remove.append(token)
for token in to_remove:
idx = bisect.bisect_left(self._ring, token)
if idx < len(self._ring) and self._ring[idx] == token:
self._ring.pop(idx)
self._ring_map.pop(token, None)
def get_node(self, key: str) -> Tuple[str, int]:
"""Return (node_id, token_hash) responsible for this key."""
if not self._ring:
raise KeyError("No nodes in ring")
token = _hash(key)
idx = bisect.bisect(self._ring, token)
if idx == len(self._ring):
idx = 0 # wrap around
chosen = self._ring[idx]
return self._ring_map[chosen], chosenHardTechnical
23 practiced
Design a memory-efficient serialization scheme in Python for storing very large NumPy arrays with metadata and streaming writes (so you don't hold the entire array in RAM). Provide example code that writes chunks to disk and reads them back with minimal memory overhead (mention numpy.memmap, HDF5/h5py or Apache Arrow). Discuss pros/cons for ML checkpoints and input datasets.
Sample Answer
Approach: use chunked streaming writes to either a binary memory-mapped NumPy file for raw arrays (very low overhead) or HDF5 (h5py) for richer metadata, chunking, and compression. Show two examples: memmap for maximal simplicity and h5py for production flexibility. Also mention Arrow as columnar alternative (good for table-like data).NumPy memmap (streaming chunk writes)HDF5 (h5py) with chunking & compressionKey points and reasoning:- memmap: minimal overhead, simple, best for single large homogeneous arrays and very fast sequential reads/writes. No extra dependencies. But no built-in compression, limited metadata support (use sidecar JSON).- h5py/HDF5: supports chunking, compression, partial I/O, datasets, attributes, concurrent readers (single writer), and hierarchies—better for ML checkpoints (store weights + optimizer state + metadata). Slightly more complex and needs careful flush/atomicity handling.- Apache Arrow: excellent for columnar, fast IPC, zero-copy for tables and Parquet for efficient storage—useful for tabular datasets, not ideal for multi-dimensional tensors without conversion.- Streaming & memory: write in small chunks to avoid RAM spikes; when reading, use slicing (memmap or h5py) to load minibatches.- Cons for ML checkpoints: memmap lacks transactional safety (partial writes can corrupt checkpoint), HDF5 has writer locks and may be slower but safer; consider atomic rename of temporary files or use cloud object stores with multipart uploads. For distributed training, combine checkpoint shards or use frameworks' checkpoint managers.- Edge cases: dtype mismatches, interrupted writes (use .flush(), temp files + rename), concurrency, alignment when chunk sizes don't divide shape.
python
import numpy as np
import json
# metadata
meta = {"shape": (1000000, 128), "dtype": "float32", "created_by": "me"}
with open("array.meta.json","w") as f:
json.dump(meta, f)
shape = meta["shape"]; dtype = np.dtype(meta["dtype"])
# create memmap file (reserves disk without loading)
arr = np.memmap("data.dat", dtype=dtype, mode="w+", shape=shape)
# stream-write in chunks (generator yields chunk arrays)
def stream_generator():
for i in range(0, shape[0], 1000):
yield np.random.rand(min(1000, shape[0]-i), shape[1]).astype(dtype)
offset = 0
for chunk in stream_generator():
arr[offset:offset+len(chunk)] = chunk
arr.flush() # ensure data persisted
offset += len(chunk)
# later, read with memmap (no full RAM)
arr_read = np.memmap("data.dat", dtype=dtype, mode="r", shape=shape)
print(arr_read[0:5])python
import h5py
with h5py.File("data.h5","w") as f:
dset = f.create_dataset("X", shape=shape, dtype=dtype,
chunks=(1000, shape[1]), compression="gzip")
f.attrs.update(meta) # attach metadata
offset = 0
for chunk in stream_generator():
dset[offset:offset+len(chunk)] = chunk
offset += len(chunk)EasyTechnical
25 practiced
Explain how Python's dict is implemented under the hood (high level). Discuss collision handling, resizing behavior, average and worst-case complexities for lookup/insert/delete, and how these behaviors affect using dicts for large feature hashing, caches, or embedding lookups in ML pipelines.
Sample Answer
Situation (high-level): CPython's dict is a hash table using open addressing with probing and a compact array of entries (key, value, hash/index). Keys are hashed; the hash determines a starting slot in a power-of-two sized table.Collision handling: CPython uses probing (a perturb-based sequence that behaves like pseudo-quadratic probing) to find an empty slot if the primary slot is occupied. Historically tombstones (deleted markers) were used to preserve probe chains; modern implementations handle deletions and compaction to avoid buildup.Resizing behavior: When load factor passes a threshold the table is resized to a larger power-of-two and all live entries are reinserted (rehash by slot, not recomputing user hash). Resizing is O(n) work but amortized across inserts so average cost per insert is O(1).Complexities:- Average (expected): lookup/insert/delete O(1)- Worst-case: O(n) (if many collisions or an adversarial hash attack)- Memory: dicts have overhead per entry (pointer/object references, table slack)Implications for ML pipelines:- Large feature hashing: For very high-cardinality sparse features, Python dicts are memory-inefficient and can be slower than the "hashing trick" (fixed-size arrays) which yields compact, constant-memory representations suitable for vectorized ML pipelines.- Caches: Dicts are convenient for small-to-medium in-memory caches (fast average O(1)). For large caches use specialized LRU caches, sharded maps, or external key-value stores to control memory and eviction.- Embedding lookups: For dense integer indices (e.g., embedding tables), prefer contiguous arrays/tensors (NumPy/PyTorch Embedding) for memory locality and GPU acceleration. Dicts make sense for sparse embeddings or dynamic vocabularies but expect higher per-lookup overhead and worst-case unpredictability; consider hashing, index mapping, or offloading to optimized KV stores when scale/latency matter.Practical tips: ensure good hash distributions (avoid custom poor __hash__), pre-size or batch inserts to reduce resizes, and prefer vectorized/native structures for large-scale, latency-sensitive ML workloads.
Unlock Full Question Bank
Get access to hundreds of Python Coding and Data Structures interview questions and detailed answers.
Sign in to ContinueJoin thousands of developers preparing for their dream job.