Programming Deep Dive — Interview-Grade Topics¶
The "defend your knowledge under probing" companion to 12_Programming_Basics.
Every topic has: What it is → Why interviewers ask → Code with explanation → Follow-up they'll ask → Wrong answer that gets you rejected.
PART 1: JAVA DEEP DIVE¶
1.1 — equals() and hashCode() contract¶
What¶
When you override equals() in a class, you MUST also override hashCode(). They have a binding contract.
Why interviewers ask¶
This is the #1 Java bug in production code. Misusing it breaks HashMap, HashSet, and anything that relies on hashing.
The contract (memorize these 3 rules)¶
- If
a.equals(b)is true →a.hashCode() == b.hashCode()MUST be true - If
a.equals(b)is false → hashCodes MAY still be equal (collision) hashCode()must return same value across multiple calls (unless object state changes)
Code¶
public class User {
private String email;
private String name;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof User)) return false;
User u = (User) o;
return Objects.equals(email, u.email); // identity = email only
}
@Override
public int hashCode() {
return Objects.hash(email); // MUST use same field as equals
}
}
What breaks if you don't override both¶
Set<User> users = new HashSet<>();
users.add(new User("a@x.com"));
users.contains(new User("a@x.com")); // FALSE — different hashCode!
Follow-up they'll ask¶
- "What if
emailis null?" →Objects.hash()handles nulls safely - "Why use
instanceofand notgetClass()?" →instanceofallows subclasses to be equal;getClass()is stricter. Choice depends on inheritance design. - "What's the third equals rule (after reflexive, symmetric)?" → Transitive: if a=b and b=c, then a=c
Wrong answer that gets you rejected¶
"I override equals to check fields and that's enough." Why wrong: Without
hashCode()override, hash-based collections silently break.
1.2 — HashMap internals (Java 8+)¶
What¶
Hash table implementation. Key → bucket via hash function. Collisions handled with linked list, upgraded to red-black tree if too many collisions.
Why interviewers ask¶
Tests if you understand performance characteristics and why hashCode matters.
How it works step by step¶
put(key, value)callshash(key)— uses key'shashCode()- Index =
hash & (n - 1)where n = bucket array size (always power of 2) - If bucket empty → store
- If bucket has entry → check
equals()to see if it's update or collision - If collision → append to linked list at that bucket
- If list grows to 8+ nodes → convert to red-black tree (O(log n) lookup)
- If load factor > 0.75 → resize array (doubles size, rehashes)
Code reading map internals¶
HashMap<String, Integer> map = new HashMap<>();
// Default capacity: 16 buckets
// Default load factor: 0.75
// Resize triggers at: 16 * 0.75 = 12 entries
map.put("hello", 1);
// 1. hash = hash("hello".hashCode())
// 2. index = hash & 15 (for 16 buckets)
// 3. bucket[index] = new Node("hello", 1)
Follow-up they'll ask¶
- "What's the time complexity of get()?" → O(1) average, O(log n) worst (with treeification), O(n) worst-worst (terrible hashCode)
- "What's load factor?" → Ratio of entries to capacity. Default 0.75 balances memory vs collision rate.
- "What happens during resize?" → New array (2x size), every entry rehashed. Expensive — pre-size if you know count.
- "Why bucket size = power of 2?" → Allows
hash & (n-1)instead of slow modulo. Bit math is faster. - "HashMap vs Hashtable vs ConcurrentHashMap?" → Hashtable is old, synchronized everything (slow). ConcurrentHashMap uses segment locking for thread-safe high performance.
Wrong answer that gets you rejected¶
"HashMap is O(1) always." Why wrong: Only on average. With bad hashCode, all entries collide → O(n).
1.3 — ArrayList vs LinkedList (real performance)¶
What¶
Both implement List but with very different underlying structures.
Why interviewers ask¶
Common trick question. The "obvious" answer (LinkedList is faster for inserts) is often wrong in practice.
Comparison¶
| Operation | ArrayList | LinkedList |
|---|---|---|
get(index) |
O(1) | O(n) — must traverse |
add(element) (to end) |
O(1) amortized | O(1) |
add(0, element) (to start) |
O(n) — shifts all | O(1) |
add(middle, element) |
O(n) — shift | O(n) — traverse to find spot |
remove(0) |
O(n) | O(1) |
| Memory per element | low (array slot) | high (node + prev + next pointers) |
| Cache locality | excellent | terrible |
The truth (most candidates miss this)¶
ArrayList wins in practice almost always. Reasons: 1. CPU cache loves contiguous memory (ArrayList) — even O(n) operations on ArrayList beat O(1) on LinkedList because of cache misses 2. LinkedList traversal pollutes the cache 3. Modern CPUs are wildly faster at sequential memory access
Code — when LinkedList actually wins¶
// LinkedList is fast as a Deque (queue from both ends)
Deque<String> queue = new LinkedList<>();
queue.addFirst("a");
queue.addLast("b");
queue.removeFirst();
// But ArrayDeque is usually even better!
Follow-up they'll ask¶
- "When would you actually use LinkedList?" → Honestly, rarely. As a
Deque, preferArrayDeque. - "Why is
ArrayList.add()O(1) amortized?" → Most adds are O(1). When full, it doubles capacity (O(n) rare event). Averaged over many calls: O(1). - "How does ArrayList grow?" → Default: 50% growth (was 100% in older Java). When full, new array allocated, contents copied.
Wrong answer that gets you rejected¶
"LinkedList is better for frequent insertions." Why wrong: Theory says yes, real-world CPU cache says no — except at the very ends.
1.4 — Why String is immutable¶
What¶
You cannot modify a String. All "modifications" create new Strings.
Why immutable?¶
- String pool — JVM caches string literals to save memory. Mutability would break sharing.
- Security — strings used in file paths, URLs, class names. Immutability prevents tampering after validation.
- Hashing —
String.hashCode()cached after first call. Mutability would invalidate. - Thread safety — immutable objects are inherently thread-safe.
Why interviewers ask¶
Tests if you understand JVM design philosophy.
The String pool¶
String a = "hello"; // goes into String pool
String b = "hello"; // reuses the SAME object from pool
String c = new String("hello"); // forces new object on HEAP
a == b; // true — same reference
a == c; // false — different objects
a.equals(c); // true — content match
c.intern() == a; // true — intern() returns pool reference
Follow-up they'll ask¶
- "How many String objects:
String s = new String("hi");" → Two! "hi" in pool, plus new on heap. - "What is StringBuilder vs StringBuffer?" → Both mutable. StringBuffer is synchronized (slower), StringBuilder is not (faster, single-thread). Use StringBuilder unless you need thread safety.
- "Why does
s += "x"in a loop kill performance?" → Each iteration creates new String + new char array. Use StringBuilder.
Wrong answer that gets you rejected¶
"Strings are immutable so they can't be changed." Why wrong: That's the WHAT. The interviewer wants the WHY (4 reasons above).
1.5 — Checked vs Unchecked exceptions¶
What¶
- Checked: Must be declared in
throwsor caught. Compiler enforces. ExtendsException. - Unchecked: No compile-time enforcement. Extends
RuntimeException.
Why interviewers ask¶
Tests understanding of Java's design philosophy and error handling.
Hierarchy¶
Throwable
├── Error (system, unrecoverable — OutOfMemoryError, StackOverflowError)
└── Exception
├── RuntimeException (unchecked — NullPointerException, IllegalArgumentException)
└── Other (checked — IOException, SQLException, ClassNotFoundException)
Code¶
// Checked — compiler forces you to handle
public void readFile() throws IOException { // must declare
FileReader f = new FileReader("a.txt");
}
// Or catch
public void readFile() {
try {
FileReader f = new FileReader("a.txt");
} catch (IOException e) {
// handle
}
}
// Unchecked — no compiler enforcement
public void divide(int a, int b) {
int x = a / b; // throws ArithmeticException if b=0, no declaration needed
}
When to use which¶
- Checked for recoverable problems (file missing → user can retry)
- Unchecked for programming errors (null reference, illegal argument)
Follow-up they'll ask¶
- "Why do many devs hate checked exceptions?" → Forces verbose
throwschains. Breaks lambdas (lambda can't throw checked). Modern libraries (Spring, Jakarta) lean unchecked. - "What does
try-with-resourcesdo?" → Auto-callsclose()on resources, even if exception thrown. RequiresAutoCloseable. - "What's exception chaining?" →
throw new MyException("wrapper", originalException)preserves cause.
Wrong answer that gets you rejected¶
"Errors and Exceptions are the same." Why wrong: Error = JVM-level, don't catch. Exception = app-level, can handle.
1.6 — Stream API (Java 8+)¶
What¶
Functional-style operations on collections. Lazy, chainable, supports parallel.
Why interviewers ask¶
Modern Java code uses streams heavily. Confirms you're not stuck in Java 6.
Core pattern¶
List<String> names = List.of("Rohan", "Asha", "Bob", "Carol");
List<String> result = names.stream()
.filter(n -> n.length() > 3) // intermediate (lazy)
.map(String::toUpperCase) // intermediate (lazy)
.sorted() // intermediate (lazy)
.collect(Collectors.toList()); // terminal (triggers)
// [ASHA, CAROL, ROHAN]
Operations¶
Intermediate (return Stream, lazy):
- filter(predicate) — keep matching
- map(function) — transform each
- flatMap(function) — flatten nested
- sorted(), distinct(), limit(n), skip(n)
Terminal (trigger execution):
- collect(collector) — to List, Set, Map
- forEach(action) — side effect
- count(), sum(), min(), max()
- reduce(identity, accumulator)
- anyMatch(), allMatch(), noneMatch()
- findFirst(), findAny() — returns Optional
Real example¶
// Total age of adult users
int totalAge = users.stream()
.filter(u -> u.getAge() >= 18)
.mapToInt(User::getAge)
.sum();
// Group by city
Map<String, List<User>> byCity = users.stream()
.collect(Collectors.groupingBy(User::getCity));
// Average response time
double avg = responses.stream()
.mapToLong(Response::getTimeMs)
.average()
.orElse(0);
Method references¶
| Syntax | Meaning |
|---|---|
String::toUpperCase |
s -> s.toUpperCase() |
System.out::println |
x -> System.out.println(x) |
User::new |
() -> new User() |
User::getName |
u -> u.getName() |
Follow-up they'll ask¶
- "Stream vs Collection?" → Collection holds data. Stream processes data. Streams are lazy and single-use.
- "What's parallelStream?" → Splits into chunks, processes on ForkJoinPool. Use only for CPU-bound, large datasets. Beware of side effects.
- "What does Collector do?" → Defines how to accumulate stream elements.
Collectors.toList(),toMap(),groupingBy(),joining(",").
Wrong answer that gets you rejected¶
"I always use streams instead of for loops." Why wrong: Streams have overhead. For simple iteration, a for-loop is faster and clearer.
1.7 — Functional Interfaces¶
What¶
An interface with exactly one abstract method (SAM — Single Abstract Method). Can be used as lambda target.
Why interviewers ask¶
Foundation of lambdas and Stream API.
The Big 4¶
// 1. Function<T, R> — take T, return R
Function<String, Integer> length = s -> s.length();
length.apply("hello"); // 5
// 2. Predicate<T> — take T, return boolean
Predicate<Integer> isEven = n -> n % 2 == 0;
isEven.test(4); // true
// 3. Consumer<T> — take T, return void
Consumer<String> printer = s -> System.out.println(s);
printer.accept("hi");
// 4. Supplier<T> — take nothing, return T
Supplier<LocalDate> today = () -> LocalDate.now();
today.get();
Custom functional interface¶
@FunctionalInterface // optional but recommended (compiler check)
interface Calculator {
int calculate(int a, int b);
}
Calculator add = (a, b) -> a + b;
add.calculate(2, 3); // 5
Follow-up they'll ask¶
- "What's BiFunction?" → Like Function but takes 2 args:
(a, b) -> result - "UnaryOperator?" → Function
— same input/output type - "Why @FunctionalInterface annotation?" → Compiler error if interface has more than one abstract method
1.8 — Generics (bounded types, wildcards, type erasure)¶
What¶
Type parameters that let you write classes/methods working with any type while preserving type safety.
Why interviewers ask¶
Common source of bugs and confusion. Tests deep Java knowledge.
Basic generic class¶
public class Box<T> {
private T value;
public void set(T value) { this.value = value; }
public T get() { return value; }
}
Box<String> b = new Box<>();
b.set("hello");
// b.set(42); // compile error — type safety!
Bounded types¶
// T must be Number or subclass
public class NumberBox<T extends Number> {
public double doubled() { return value.doubleValue() * 2; }
}
NumberBox<Integer> ok = new NumberBox<>();
// NumberBox<String> bad = new NumberBox<>(); // compile error
Wildcards¶
// ? extends — covariance (read-only from list's perspective)
public double sum(List<? extends Number> list) {
double total = 0;
for (Number n : list) total += n.doubleValue();
return total;
// list.add(...) // ERROR — can't add to ? extends
}
// ? super — contravariance (write-only)
public void addNumbers(List<? super Integer> list) {
list.add(1); // OK
list.add(2);
}
// PECS mnemonic: Producer Extends, Consumer Super
// If you READ from collection — use extends
// If you WRITE to collection — use super
Type erasure (tricky!)¶
At runtime, generic types are erased to Object. The compiler enforces types at compile time, then strips them.
List<String> a = new ArrayList<>();
List<Integer> b = new ArrayList<>();
a.getClass() == b.getClass(); // true — both are ArrayList at runtime!
// Cannot do:
public <T> T create() {
return new T(); // ERROR — T erased
}
public boolean check(Object o) {
return o instanceof List<String>; // ERROR
}
Follow-up they'll ask¶
- "Why type erasure?" → Backward compatibility with pre-generics Java
- "What's a raw type?" →
List(no<>) — bypasses type checking, dangerous - "Diff between
List<Object>andList<?>?" →List<Object>only accepts List
Wrong answer that gets you rejected¶
"Generics give runtime type safety." Why wrong: Only compile-time. At runtime,
List<String>andList<Integer>are identical.
1.9 — Comparable vs Comparator¶
What¶
Both for sorting. Comparable = natural ordering on the class itself. Comparator = external custom ordering.
Code¶
// Comparable — sort User by name naturally
public class User implements Comparable<User> {
String name;
@Override
public int compareTo(User other) {
return this.name.compareTo(other.name);
}
}
Collections.sort(userList); // uses User's natural order
// Comparator — custom ordering, no class change needed
Comparator<User> byAge = (a, b) -> Integer.compare(a.age, b.age);
Collections.sort(userList, byAge);
// Modern syntax
userList.sort(Comparator.comparing(User::getAge));
userList.sort(Comparator.comparing(User::getAge).reversed());
userList.sort(Comparator.comparing(User::getCity)
.thenComparing(User::getAge));
Contract: compareTo / compare¶
- Negative if
a < b - Zero if
a == b - Positive if
a > b
Follow-up they'll ask¶
- "When to use which?" → Comparable for the one "natural" ordering. Comparator for multiple custom orderings.
- "Must compareTo be consistent with equals?" → Strongly recommended. Inconsistency breaks TreeMap/TreeSet.
1.10 — Concurrency basics¶
What¶
Java has built-in multithreading. Tests touch threads, locks, atomic operations.
Thread creation¶
// Old way
Thread t = new Thread(() -> System.out.println("Hi"));
t.start();
// Modern — ExecutorService
ExecutorService pool = Executors.newFixedThreadPool(4);
pool.submit(() -> doWork());
Future<String> f = pool.submit(() -> "result");
String result = f.get(); // blocks
pool.shutdown();
synchronized¶
// Method-level lock
public synchronized void increment() {
count++;
}
// Block-level lock
public void increment() {
synchronized (this) {
count++;
}
}
volatile¶
Ensures visibility across threads (writes are immediately visible to other threads). Does NOT ensure atomicity!
private volatile boolean stopFlag = false;
// Thread A sets stopFlag = true
// Thread B sees the change immediately
Atomic classes¶
AtomicInteger count = new AtomicInteger(0);
count.incrementAndGet(); // thread-safe, no lock needed
count.compareAndSet(5, 10); // CAS operation
Follow-up they'll ask¶
- "synchronized vs volatile?" → synchronized = mutex + visibility. volatile = visibility only.
- "What's a race condition?" → Two threads access shared state, at least one writes, no synchronization → unpredictable result
- "Deadlock?" → Thread A holds lock X waiting for Y; Thread B holds Y waiting for X. Both stuck forever.
- "ConcurrentHashMap vs synchronized HashMap?" → ConcurrentHashMap uses bucket-level locking → way faster under contention.
Wrong answer that gets you rejected¶
"volatile makes operations atomic." Why wrong: volatile = visibility only.
count++on volatile is still NOT atomic (read-modify-write).
1.11 — final keyword (3 uses)¶
// 1. final variable — cannot reassign
final int MAX = 100;
final List<String> list = new ArrayList<>();
list.add("x"); // OK — modifying contents
// list = new ArrayList<>(); // ERROR — reassignment
// 2. final method — cannot override in subclass
public class Animal {
public final void breathe() { ... }
}
// 3. final class — cannot be extended
public final class String { ... } // hence we can't subclass String
1.12 — Singleton pattern (interview classic)¶
// Lazy + thread-safe (double-checked locking)
public class Singleton {
private static volatile Singleton instance;
private Singleton() {} // private constructor
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
// Modern — enum (Joshua Bloch's recommended approach)
public enum Singleton {
INSTANCE;
public void doWork() { ... }
}
PART 2: PYTHON DEEP DIVE¶
2.1 — List comprehensions vs map/filter¶
What¶
Concise way to build lists/dicts/sets from iterables. Pythonic alternative to map/filter.
Why interviewers ask¶
Differentiates Python beginners from those who write idiomatic code.
List comprehension¶
# Basic
squares = [n ** 2 for n in range(10)]
# [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
# With filter
evens = [n for n in range(10) if n % 2 == 0]
# [0, 2, 4, 6, 8]
# Nested
pairs = [(x, y) for x in [1, 2] for y in [3, 4]]
# [(1,3), (1,4), (2,3), (2,4)]
# vs map/filter (less Pythonic)
squares = list(map(lambda n: n ** 2, range(10)))
evens = list(filter(lambda n: n % 2 == 0, range(10)))
Dict and set comprehensions¶
# Dict
sq_map = {n: n ** 2 for n in range(5)}
# {0: 0, 1: 1, 2: 4, 3: 9, 4: 16}
# Set
uniques = {n % 3 for n in range(10)}
# {0, 1, 2}
Generator expression (key difference!)¶
# List comp — materializes ALL values immediately
nums = [n ** 2 for n in range(10**6)] # big memory
# Generator — lazy, one at a time
nums = (n ** 2 for n in range(10**6)) # parens! memory efficient
sum(nums) # iterates lazily
Follow-up they'll ask¶
- "When to use comprehension vs map?" → Comprehension is Pythonic for transform + filter. Map for existing functions:
map(int, str_list). - "What's the memory impact?" → List comp = O(n). Generator = O(1). Use generator for large data.
Wrong answer that gets you rejected¶
"Comprehensions and generators are the same." Why wrong: Comprehension creates the full list. Generator is lazy.
2.2 — Generators and yield¶
What¶
A function that produces values lazily, one at a time, instead of returning a full list.
Why interviewers ask¶
Critical for memory-efficient processing of large data streams.
Basic generator¶
def counter(n):
i = 0
while i < n:
yield i # pauses here, resumes on next()
i += 1
g = counter(3)
next(g) # 0
next(g) # 1
next(g) # 2
next(g) # StopIteration
# Or just loop
for n in counter(3):
print(n)
Real use case — process a huge file¶
def read_lines(file_path):
with open(file_path) as f:
for line in f: # file objects are themselves generators
yield line.strip()
# Process 10 GB file without loading all into memory
for line in read_lines("huge.log"):
if "ERROR" in line:
print(line)
Generators as state machines¶
def running_total():
total = 0
while True:
x = yield total # receives via .send()
if x is None:
break
total += x
g = running_total()
next(g) # 0 (initial yield)
g.send(5) # 5
g.send(3) # 8
g.send(10) # 18
Follow-up they'll ask¶
- "yield vs return?" → return ends function, gives single value. yield pauses function, can resume.
- "Generator vs iterator?" → Generator is a special, simpler kind of iterator. All generators are iterators; not vice versa.
- "Why use generators?" → Memory efficiency. Lazy evaluation. Pipelines of transformations.
2.3 — Decorators (must-know)¶
What¶
A function that wraps another function to extend its behavior, without modifying it.
Why interviewers ask¶
Decorators are everywhere in Python — Flask routes, pytest fixtures, caching, timing, logging.
Basic decorator¶
def my_decorator(func):
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__}")
result = func(*args, **kwargs)
print(f"Done {func.__name__}")
return result
return wrapper
@my_decorator
def greet(name):
return f"Hello, {name}"
greet("Rohan")
# Calling greet
# Done greet
What @ syntax actually does¶
# These are equivalent:
@my_decorator
def greet(name): ...
# Same as:
def greet(name): ...
greet = my_decorator(greet)
Real-world: caching/memoization¶
def cache(fn):
memo = {}
def wrapper(x):
if x not in memo:
memo[x] = fn(x)
return memo[x]
return wrapper
@cache
def fib(n):
if n < 2: return n
return fib(n - 1) + fib(n - 2)
fib(100) # fast, no decorator → would take forever
Decorator with arguments¶
def repeat(times):
def decorator(fn):
def wrapper(*args, **kwargs):
for _ in range(times):
fn(*args, **kwargs)
return wrapper
return decorator
@repeat(3)
def say_hi():
print("Hi")
say_hi() # prints Hi 3 times
Preserve metadata with functools.wraps¶
from functools import wraps
def my_decorator(fn):
@wraps(fn) # preserves fn's __name__, __doc__
def wrapper(*args, **kwargs):
return fn(*args, **kwargs)
return wrapper
Follow-up they'll ask¶
- "Why @wraps?" → Without it, the decorated function loses its original name and docstring → breaks introspection, debugging.
- "Decorator vs middleware?" → Same concept. Middleware is the web framework word for it.
- "Can you chain decorators?" → Yes.
@a @b def f(): ...isf = a(b(f))— applied bottom-up.
2.4 — Context managers (with statement)¶
What¶
Handles setup + teardown automatically. Most famous: file handling.
Why interviewers ask¶
Shows you write clean, leak-free code.
Built-in usage¶
# Without context manager — must remember to close
f = open("file.txt")
data = f.read()
f.close() # easy to forget; doesn't run if exception
# With context manager — auto close
with open("file.txt") as f:
data = f.read()
# f.close() called automatically, even on exception
Write your own¶
class Timer:
def __enter__(self):
self.start = time.time()
return self
def __exit__(self, exc_type, exc_val, exc_tb):
elapsed = time.time() - self.start
print(f"Elapsed: {elapsed:.2f}s")
return False # don't suppress exceptions
with Timer():
expensive_operation()
Easier with @contextmanager¶
from contextlib import contextmanager
@contextmanager
def timer():
start = time.time()
try:
yield
finally:
print(f"Elapsed: {time.time() - start:.2f}s")
with timer():
expensive_operation()
Real test usage¶
# Pytest temporary directories
with tempfile.TemporaryDirectory() as tmp:
write_file(tmp)
# Folder auto-deleted
# Mocking
with mock.patch("requests.get") as mock_get:
mock_get.return_value.json.return_value = {"id": 1}
result = my_function()
Follow-up they'll ask¶
- "What's enter and exit?" → The protocol that makes
withwork. Any class implementing both can be used inwith. - "What if exit returns True?" → Suppresses exception. Use carefully — often a bad idea.
2.5 — GIL (Global Interpreter Lock)¶
What¶
A mutex in CPython that allows only ONE thread to execute Python bytecode at a time.
Why interviewers ask¶
Common source of confusion. Affects how you do concurrency in Python.
Why GIL exists¶
- Simplifies CPython's reference counting (memory management)
- Makes single-threaded code faster
- Removing it would break tons of C extensions
What it means in practice¶
import threading
def cpu_work():
n = 0
for i in range(10**8):
n += i
# Spinning up 4 threads for CPU work does NOT speed up
threads = [threading.Thread(target=cpu_work) for _ in range(4)]
# Same time as running serially — GIL prevents parallelism
When GIL doesn't hurt (I/O bound)¶
# Threads ARE useful for I/O — GIL is released during I/O wait
def fetch_url(url):
return requests.get(url).text # GIL released during network wait
threads = [threading.Thread(target=fetch_url, args=(url,)) for url in urls]
# Much faster than serial — threads run concurrently during I/O
CPU-bound? Use multiprocessing¶
from multiprocessing import Pool
with Pool(4) as p:
results = p.map(cpu_work, items)
# Separate processes = separate GILs = true parallelism
Follow-up they'll ask¶
- "Why is multiprocessing 'true' parallel?" → Each process has its own Python interpreter and GIL.
- "Is GIL going away?" → PEP 703 (no-GIL Python) is in progress — experimental in Python 3.13+.
- "What about asyncio?" → Single-threaded but uses cooperative multitasking — great for I/O concurrency.
Wrong answer that gets you rejected¶
"Threading doesn't work in Python." Why wrong: It works great for I/O. Just not for CPU-bound work.
2.6 — args / *kwargs deep dive¶
What¶
*args— collects positional arguments into a tuple**kwargs— collects keyword arguments into a dict
Code¶
def example(*args, **kwargs):
print(args) # tuple
print(kwargs) # dict
example(1, 2, 3, name="Rohan", age=28)
# (1, 2, 3)
# {'name': 'Rohan', 'age': 28}
Unpacking (opposite direction)¶
nums = [1, 2, 3]
print(*nums) # print(1, 2, 3)
config = {"a": 1, "b": 2}
some_func(**config) # some_func(a=1, b=2)
Forwarding to wrapped function¶
Follow-up they'll ask¶
- "Order of args in signature?" →
def f(pos, *args, kw_only, **kwargs)— pos, then args, then keyword-only, then *kwargs - "What's a keyword-only argument?" → After
*in signature:def f(a, *, b)— b MUST be passed asb=...
2.7 — Magic methods (dunder methods)¶
What¶
Special methods with double underscores. Define how objects behave with built-in operations.
Why interviewers ask¶
Tests if you can write Pythonic, idiomatic classes.
Most important magic methods¶
class User:
def __init__(self, name, age): # constructor
self.name = name
self.age = age
def __str__(self): # human-readable
return f"{self.name} ({self.age})"
def __repr__(self): # developer-readable, unambiguous
return f"User(name='{self.name}', age={self.age})"
def __eq__(self, other): # ==
if not isinstance(other, User):
return False
return self.name == other.name
def __hash__(self): # for use in sets/dicts
return hash(self.name)
def __lt__(self, other): # < (also __le__, __gt__, __ge__)
return self.age < other.age
def __len__(self): # len(user)
return len(self.name)
def __getitem__(self, key): # user[key]
return self.__dict__[key]
def __call__(self, *args): # user() — makes instance callable
print(f"Called with {args}")
str vs repr (common Q)¶
__str__— for end users (print(obj)calls this)__repr__— for developers (debugger,repr(obj), REPL)- If only one, define
__repr__— it's the fallback for__str__
Follow-up they'll ask¶
- "Why both eq and hash?" → Same contract as Java. Equal objects must have equal hashes.
- "What's new vs init?" →
__new__creates the object.__init__initializes it. You rarely override new.
2.8 — Mutable default argument trap (classic gotcha)¶
The bug¶
def add_item(item, lst=[]): # BUG! Default list is shared!
lst.append(item)
return lst
add_item(1) # [1]
add_item(2) # [1, 2] — same list reused!
add_item(3) # [1, 2, 3]
Why it happens¶
Default values are evaluated ONCE at function definition. The empty list is created at definition, shared across all calls without a fresh list passed in.
The fix¶
Why interviewers ask¶
This catches even experienced Python devs. Tests deep understanding of evaluation timing.
2.9 — Shallow vs Deep copy¶
What¶
- Shallow copy: New container, but inner objects are shared references
- Deep copy: Recursively copies everything
Code¶
import copy
original = [[1, 2], [3, 4]]
shallow = copy.copy(original)
# or: shallow = original[:]
# or: shallow = list(original)
shallow[0].append(99)
print(original) # [[1, 2, 99], [3, 4]] — inner list modified!
deep = copy.deepcopy(original)
deep[0].append(100)
print(original) # unchanged
When to use¶
- Shallow: when contents are immutable (numbers, strings)
- Deep: when contents are mutable and you need full independence
2.10 — is vs ==¶
Difference¶
==— equality (calls__eq__)is— identity (same object in memory)
a = [1, 2]
b = [1, 2]
c = a
a == b # True (content equal)
a is b # False (different objects)
a is c # True (same object)
# Use 'is' for None
x is None # CORRECT
x == None # works but bad style
Gotcha: small integer caching¶
a = 100
b = 100
a is b # True — CPython caches small ints (-5 to 256)
a = 1000
b = 1000
a is b # False — outside cache range
2.11 — Iterator vs Iterable vs Generator¶
Definitions¶
- Iterable: Has
__iter__returning an iterator. Examples: list, dict, set, str. - Iterator: Has
__next__returning next value. Created byiter(iterable). - Generator: Special iterator created with
yield.
Code¶
lst = [1, 2, 3] # iterable
it = iter(lst) # iterator
next(it) # 1
next(it) # 2
next(it) # 3
next(it) # StopIteration
# All these work with for-loop because they're iterable:
for x in [1, 2, 3]: ... # list
for x in "hello": ... # string
for x in {"a": 1}: ... # dict (iterates keys)
for x in (n*2 for n in [1,2]): # generator
2.12 — Multiple inheritance & MRO¶
What¶
Python supports multiple inheritance. MRO = Method Resolution Order — what order to look up methods.
Code¶
class A:
def greet(self): print("A")
class B(A):
def greet(self): print("B")
class C(A):
def greet(self): print("C")
class D(B, C):
pass
d = D()
d.greet() # B
print(D.__mro__)
# (D, B, C, A, object)
MRO uses C3 linearization — left-to-right, depth-first, no duplicates.¶
PART 3: JAVASCRIPT DEEP DIVE¶
3.1 — Event loop (THE most important JS concept)¶
What¶
JavaScript is single-threaded but handles async via the event loop. Tasks queue up and the engine processes them one at a time.
Why interviewers ask¶
The #1 differentiator between junior and mid-level JS devs.
Components¶
- Call Stack — currently executing code
- Web APIs / Node APIs — browser/Node features (setTimeout, fetch, fs)
- Microtask Queue — Promise callbacks, queueMicrotask
- Macrotask Queue — setTimeout, setInterval, I/O, UI rendering
The rule¶
After every macrotask, process ALL microtasks before the next macrotask.
Classic interview trick¶
console.log(1);
setTimeout(() => console.log(2), 0);
Promise.resolve().then(() => console.log(3));
console.log(4);
// Output: 1, 4, 3, 2
Why this order:
1. console.log(1) — synchronous, runs immediately → 1
2. setTimeout — registers callback to macrotask queue
3. Promise.then — registers callback to microtask queue
4. console.log(4) — synchronous → 4
5. Stack empty → drain microtasks → 3
6. Then macrotasks → 2
Follow-up they'll ask¶
- "Why microtasks before macrotasks?" → Promise resolutions should happen ASAP for responsive UI
- "What's
queueMicrotask?" → Schedules a microtask explicitly (rarely needed in app code) - "
setTimeout(fn, 0)— does it run immediately?" → No! It's queued as macrotask, runs after current sync code + all microtasks
3.2 — Hoisting (var vs let vs const vs function)¶
What¶
JS "moves" declarations to the top of the scope at compile time. Behavior differs by declaration type.
var is hoisted as undefined¶
console.log(x); // undefined (NOT error)
var x = 5;
// JS engine sees this as:
// var x;
// console.log(x);
// x = 5;
let and const are hoisted but in "Temporal Dead Zone"¶
Function declarations fully hoisted¶
Function expressions NOT hoisted¶
Why interviewers ask¶
Catches devs who don't understand JS scoping.
3.3 — Closures¶
What¶
A function that "remembers" variables from its outer scope, even after the outer function returns.
Why interviewers ask¶
The most important JS concept after the event loop. Used everywhere in real code.
Basic¶
function makeCounter() {
let count = 0;
return function() {
count++;
return count;
};
}
const counter = makeCounter();
counter(); // 1
counter(); // 2
counter(); // 3
// count is "trapped" inside the closure
Real-world: data privacy¶
function createBankAccount(initial) {
let balance = initial; // private
return {
deposit(amount) { balance += amount; },
withdraw(amount) {
if (amount > balance) throw "Insufficient";
balance -= amount;
},
getBalance() { return balance; }
};
}
const account = createBankAccount(100);
account.deposit(50);
account.getBalance(); // 150
account.balance; // undefined — truly private
Real-world: debounce (commonly asked!)¶
function debounce(fn, ms) {
let timeoutId;
return function(...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => fn(...args), ms);
};
}
const onSearch = debounce(searchAPI, 300);
// Calling onSearch repeatedly only fires the last call after 300ms idle
Real-world: throttle¶
function throttle(fn, ms) {
let lastCalled = 0;
return function(...args) {
const now = Date.now();
if (now - lastCalled >= ms) {
lastCalled = now;
fn(...args);
}
};
}
Closure gotcha in loops¶
// BUG with var
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0);
}
// Prints: 3, 3, 3 — all closures share the same i
// FIX with let
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0);
}
// Prints: 0, 1, 2 — let creates new binding per iteration
Follow-up they'll ask¶
- "Memory implications of closures?" → They keep referenced vars alive. Forgotten closures can cause memory leaks.
- "Implement once()" → Function that can only be called one time
3.4 — Prototypes & Prototypal Inheritance¶
What¶
JS uses prototypes, not classes (under the hood). Every object has an internal [[Prototype]] link to another object.
Why interviewers ask¶
class syntax (ES6) hides the truth. Senior devs must know the underlying model.
How it works¶
const animal = {
breathe() { console.log("Breathing"); }
};
const dog = Object.create(animal); // dog's prototype = animal
dog.bark = function() { console.log("Woof"); };
dog.bark(); // Woof — found on dog
dog.breathe(); // Breathing — not on dog, looks up prototype chain
// Chain: dog → animal → Object.prototype → null
Class is syntactic sugar¶
class Animal {
breathe() { console.log("Breathing"); }
}
class Dog extends Animal {
bark() { console.log("Woof"); }
}
// Equivalent to:
function Animal() {}
Animal.prototype.breathe = function() { console.log("Breathing"); };
function Dog() {}
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.bark = function() { console.log("Woof"); };
Follow-up they'll ask¶
- "What's proto?" → Direct access to the prototype link (use
Object.getPrototypeOfinstead in modern code) - "prototype vs proto?" →
prototypeis a property on constructor functions.__proto__is on instances. - "How does method lookup work?" → JS walks the prototype chain. Stops at first match or returns undefined.
3.5 — this binding (call, apply, bind)¶
The 4 rules of this¶
// 1. Default — `this` = global (or undefined in strict mode)
function f() { console.log(this); }
f(); // window/global (or undefined)
// 2. Implicit — `this` = object before the dot
const obj = { name: "Rohan", greet() { console.log(this.name); } };
obj.greet(); // "Rohan"
// 3. Explicit — call, apply, bind
function greet(greeting) { console.log(`${greeting}, ${this.name}`); }
const user = { name: "Rohan" };
greet.call(user, "Hi"); // Hi, Rohan
greet.apply(user, ["Hello"]); // Hello, Rohan
const bound = greet.bind(user); // returns new function
bound("Hey"); // Hey, Rohan
// 4. new keyword
function User(name) { this.name = name; }
const u = new User("Rohan");
Arrow function exception¶
Arrow functions don't have their own this. They inherit from surrounding scope.
const obj = {
name: "Rohan",
greet: function() {
setTimeout(function() {
console.log(this.name); // undefined — `this` lost
}, 100);
setTimeout(() => {
console.log(this.name); // "Rohan" — arrow inherits
}, 100);
}
};
Follow-up they'll ask¶
- "When should you NOT use arrow functions?" → As object methods, constructors, or when you need dynamic
this - "call vs apply vs bind?" → call/apply invoke immediately; bind returns a new function. call takes args separately, apply takes array.
3.6 — Promises in depth¶
Promise states¶
- Pending — initial state
- Fulfilled — operation succeeded (
.then()runs) - Rejected — operation failed (
.catch()runs) - Settled — either fulfilled or rejected
Creating promises¶
const p = new Promise((resolve, reject) => {
setTimeout(() => {
if (Math.random() > 0.5) resolve("success");
else reject("fail");
}, 1000);
});
p.then(data => console.log(data))
.catch(err => console.error(err))
.finally(() => console.log("always"));
Promise.all vs allSettled vs race vs any¶
// Promise.all — fails fast on first rejection
const [a, b, c] = await Promise.all([fetchA(), fetchB(), fetchC()]);
// Promise.allSettled — waits for all, returns results array
const results = await Promise.allSettled([fetchA(), fetchB()]);
// [{status: "fulfilled", value: ...}, {status: "rejected", reason: ...}]
// Promise.race — first to settle (resolve or reject) wins
const fastest = await Promise.race([fetchA(), fetchB()]);
// Promise.any — first to RESOLVE wins (ignores rejections until all fail)
const firstSuccess = await Promise.any([fetchA(), fetchB()]);
Common interview problem: implement Promise.all¶
function promiseAll(promises) {
return new Promise((resolve, reject) => {
const results = [];
let completed = 0;
promises.forEach((p, i) => {
Promise.resolve(p)
.then(value => {
results[i] = value;
completed++;
if (completed === promises.length) resolve(results);
})
.catch(reject);
});
});
}
async/await is syntax over Promises¶
// These are equivalent:
async function getUser() {
const res = await fetch("/api/user");
return res.json();
}
// Same as:
function getUser() {
return fetch("/api/user").then(res => res.json());
}
Common gotcha — forgetting await¶
async function bad() {
const data = fetch("/api/data"); // BUG — no await
console.log(data); // Promise object, not data
}
Follow-up they'll ask¶
- "What if Promise.all rejects? Do the other promises stop?" → No — they run to completion. Promise.all just rejects with first error.
- "Difference between Promise.race and Promise.any?" → race = first to settle (success or fail). any = first to SUCCEED.
3.7 — Destructuring patterns¶
Array destructuring¶
const [a, b, c] = [1, 2, 3];
const [first, ...rest] = [1, 2, 3, 4]; // first=1, rest=[2,3,4]
const [a, , c] = [1, 2, 3]; // skip middle
const [a = 10] = []; // default value
Object destructuring¶
const { name, age } = { name: "Rohan", age: 28 };
const { name: userName } = obj; // rename
const { city = "Bangalore" } = obj; // default
// Nested
const { user: { address: { city } } } = data;
// Function parameter destructuring (super common!)
function greet({ name, greeting = "Hi" } = {}) {
return `${greeting}, ${name}`;
}
greet({ name: "Rohan" });
Swapping variables¶
3.8 — Spread vs Rest operators¶
Both use ... but in different contexts.
Spread — expands¶
const arr1 = [1, 2, 3];
const arr2 = [...arr1, 4, 5]; // [1,2,3,4,5]
const obj1 = { a: 1 };
const obj2 = { ...obj1, b: 2 }; // {a:1, b:2}
function add(a, b, c) { return a + b + c; }
add(...[1, 2, 3]); // spread as args
Rest — collects¶
function sum(...nums) { // rest params
return nums.reduce((a, b) => a + b, 0);
}
sum(1, 2, 3, 4); // nums=[1,2,3,4]
const [first, ...rest] = [1, 2, 3]; // rest in destructuring
const { a, ...others } = { a: 1, b: 2, c: 3 };
Memory hook¶
- Spread: ... on the right side (expanding)
- Rest: ... on the left side (collecting)
3.9 — Module systems (CommonJS vs ESM)¶
CommonJS (Node.js traditional)¶
// math.js
function add(a, b) { return a + b; }
module.exports = { add };
// or
exports.add = add;
// main.js
const { add } = require("./math");
ES Modules (modern standard)¶
// math.js
export function add(a, b) { return a + b; }
export default function multiply(a, b) { return a * b; }
// main.js
import multiply, { add } from "./math.js";
import * as math from "./math.js";
Key differences¶
| CommonJS | ESM | |
|---|---|---|
| Syntax | require/module.exports |
import/export |
| Loading | Synchronous | Asynchronous |
| When evaluated | Runtime | Parse time |
| Tree shaking | No | Yes (smaller bundles) |
| File extension | .js |
.mjs or "type": "module" in package.json |
3.10 — Equality (== vs === vs Object.is)¶
"5" == 5 // true (type coercion)
"5" === 5 // false (strict)
null == undefined // true
null === undefined // false
NaN === NaN // false (!)
Object.is(NaN, NaN) // true
0 === -0 // true
Object.is(0, -0) // false
Rule: Always use === unless you have a specific reason.
3.11 — Common gotchas / interview traps¶
typeof null¶
typeof null; // "object" — bug from 1995, never fixed
typeof undefined; // "undefined"
typeof []; // "object"
typeof function(){}; // "function"
Array.isArray¶
Object key order¶
const obj = { z: 1, a: 2, 1: 3, 0: 4 };
Object.keys(obj); // ["0", "1", "z", "a"]
// Integer keys first (sorted), then strings (insertion order)
Hoisting + function expression bug¶
console.log(typeof foo); // "undefined"
console.log(typeof bar); // "function"
var foo = function() {};
function bar() {}
3.12 — Object.freeze, Object.assign, structuredClone¶
// Object.freeze — shallow immutability
const obj = Object.freeze({ a: 1, b: { c: 2 } });
obj.a = 99; // silently ignored (strict mode: error)
obj.b.c = 99; // WORKS — nested object not frozen
// Object.assign — shallow copy/merge
const merged = Object.assign({}, obj1, obj2); // or {...obj1, ...obj2}
// structuredClone — deep copy (modern)
const deepCopy = structuredClone(original); // works on arrays, dates, maps
// Old way: JSON.parse(JSON.stringify(obj)) — loses functions, Dates, undefined
QUICK INTERVIEW DRILL — 15 Questions¶
Try answering these from memory. Each is asked frequently.
Java¶
- What's the equals/hashCode contract?
- What's the difference between ArrayList and LinkedList — when is each faster?
- Why is String immutable?
- What does
volatileguarantee thatsynchronizeddoesn't? - What is PECS (in generics)?
Python¶
- What's the difference between a list comprehension and a generator expression?
- What does
@propertydecorator do? - What is the GIL and when does it not matter?
- Why is
def f(x=[])a bug? - What's
isvs==?
JavaScript¶
- What does
setTimeout(fn, 0)actually do? - Write a debounce function
- Why does this print
3, 3, 3? - Promise.all vs Promise.allSettled?
- What's
thisin an arrow function?
STUDY PLAN¶
| Day | Topics |
|---|---|
| 1 | Java: equals/hashCode, HashMap internals, ArrayList vs LinkedList |
| 2 | Java: Stream API, Generics, Concurrency basics |
| 3 | Python: Generators, Decorators, Context managers |
| 4 | Python: GIL, Magic methods, Mutable defaults |
| 5 | JS: Event loop, Hoisting, Closures |
| 6 | JS: Prototypes, this, Promises |
| 7 | Drill — answer the 15 questions out loud |
Pro tip: For each topic, write a 3-sentence explanation and a 5-line code example. If you can do both from memory, you've internalized it.