MODULE 1

Python — Core Idioms

Comprehensions, generators, context managers, decorators.

Comprehensions

# List, set, dict, generator comprehensions
squares = [x*x for x in range(10) if x % 2 == 0]
uniq   = {w.lower() for w in words}
by_id  = {u.id: u for u in users}
lazy   = (x*x for x in big)          # generator, O(1) memory
flat   = [c for row in matrix for c in row]   # flatten (nested for)
mat3x3 = [[0]*3 for _ in range(3)]   # correct; [[0]*3]*3 aliases!

Generators & yield

# Lazy Fibonacci
def fib():
    a, b = 0, 1
    while True: yield a; a, b = b, a+b

from itertools import islice
print(list(islice(fib(), 10)))  # first 10
# yield from — delegate
def chain_all(*its):
    for it in its: yield from it

Generators hold state as a frozen call frame; resume on next().

Context Managers

# with-block pattern for setup/teardown
from contextlib import contextmanager

@contextmanager
def timer(label):
    import time
    t0 = time.perf_counter()
    try: yield
    finally: print(f"{label}: {(time.perf_counter()-t0)*1000:.2f}ms")

with timer("work"): do_thing()

# Multiple contexts in one with
with open("a") as f, open("b","w") as g: g.write(f.read())

Decorators

from functools import wraps
def retry(n=3):
    def deco(fn):
        @wraps(fn)
        def wrapped(*a, **kw):
            for i in range(n):
                try: return fn(*a, **kw)
                except Exception:
                    if i == n-1: raise
        return wrapped
    return deco

@retry(n=5)
def flaky(): ...

Unpacking, * / **

a, *mid, z = [1,2,3,4,5]      # a=1, mid=[2,3,4], z=5
head, *rest = ["a","b","c"]
def f(**kw): pass
f(**{"x":1, "y":2})            # forward kwargs
merged = {**a, **b}            # dict merge (Python 3.9+: a | b)
combined = [*list_a, *list_b]  # list concat

Slicing

s[::-1]        # reverse
s[::2]         # every second
s[:-1]         # drop last
a, b = s[:n//2], s[n//2:]
mat[::-1]      # reverse rows
[row[:3] for row in mat]  # first 3 cols

enumerate, zip, any, all

for i, x in enumerate(xs, start=1): ...
for a, b in zip(xs, ys): ...          # truncates to shorter
# Python 3.10+: zip(..., strict=True)   # raises on unequal length
all(x > 0 for x in xs)   # short-circuit
any(p(x)  for x in xs)

Walrus :=

while (chunk := f.read(8192)): process(chunk)
if (m := re.search(r"\d+", s)): print(m.group())

List Op Complexity

Python list operation complexity
MODULE 2

Python — stdlib Essentials

Batteries interviewers expect you to use.

collections

from collections import deque, defaultdict, Counter, OrderedDict, namedtuple

q = deque([1,2,3]); q.appendleft(0); q.popleft()    # O(1) both ends
graph = defaultdict(list)
graph[1].append(2)                                   # no KeyError

c = Counter("abracadabra")
c.most_common(2)       # [('a', 5), ('b', 2)]

Point = namedtuple("Point", "x y")
p = Point(1, 2); p.x, p.y
# dataclass is usually better for new code

heapq (min-heap)

import heapq
h = []
heapq.heappush(h, 3); heapq.heappush(h, 1)
heapq.heappop(h)        # 1

# heapify in place O(n)
arr = [5,2,8,1]; heapq.heapify(arr)
heapq.nlargest(3, arr); heapq.nsmallest(3, arr)

# max-heap trick: negate values
heapq.heappush(h, -value); -heapq.heappop(h)

bisect

import bisect
a = [1,2,4,4,5]
bisect.bisect_left(a, 4)   # 2  (first ≥ 4)
bisect.bisect_right(a, 4)  # 4  (first > 4)
bisect.insort(a, 3)        # keeps sorted; O(n) due to shift

insort is O(n) — shift cost dominates.

itertools

from itertools import combinations, permutations, product, chain, accumulate, groupby, islice, cycle, repeat

list(combinations([1,2,3], 2))      # (1,2),(1,3),(2,3)
list(permutations([1,2,3], 2))      # (1,2),(1,3),(2,1),(2,3),(3,1),(3,2)
list(product([0,1], repeat=3))      # all 3-bit tuples
list(chain([1,2],[3,4]))            # flatten
list(accumulate([1,2,3,4]))         # prefix sum [1,3,6,10]
# sorted+groupby to bucket
for k, g in groupby(sorted(items, key=key), key=key):
    print(k, list(g))

functools

from functools import lru_cache, cache, reduce, partial

@lru_cache(maxsize=None)
def fib(n): return n if n < 2 else fib(n-1)+fib(n-2)

@cache          # Python 3.9+: alias for lru_cache(maxsize=None)
def f(x): ...

reduce(lambda a,b: a*b, nums, 1)
p = partial(int, base=16); p("ff")  # 255

dataclasses & typing

from dataclasses import dataclass, field
from typing import Optional

@dataclass(frozen=True, slots=True)   # Python 3.10+ for slots
class User:
    id: int
    name: str
    tags: list[str] = field(default_factory=list)  # never default = []!

# typing
def get(x: int | None) -> str:        # Python 3.10+ union syntax
    return str(x) if x is not None else ""
MODULE 3

Python — Concurrency & Internals

GIL, asyncio, the pitfalls interviewers ask about.

The GIL

CPython's Global Interpreter Lock serializes bytecode execution — only one thread runs Python bytecode at a time. Threads DO help for I/O (the lock releases on I/O syscalls); they do NOT help CPU-bound work. For CPU-bound: multiprocessing, concurrent.futures.ProcessPoolExecutor, or C-extensions releasing the GIL (numpy). Python 3.13 introduced an experimental free-threaded build; Python 3.14 (Oct 2025) promoted it to officially supported (PEP 779) via a separate free-threaded interpreter (python3.14t). Still not the default ABI — GIL remains on by default — but no longer experimental.

The lock is not held continuously: any thread blocked in a syscall (socket read, file I/O, time.sleep) releases it, and C extensions bracket heavy native work with Py_BEGIN_ALLOW_THREADS / Py_END_ALLOW_THREADS so that other Python threads keep running during the native call — while a numpy op crunches in C, another thread makes progress in Python. (Any multi-core speedup inside that op comes from BLAS's own threading, independent of the GIL.) Among pure-Python CPU threads the GIL is handed off on a timer — default sys.getswitchinterval() is 5 ms (0.005 s); the running thread is asked to drop the lock at the next bytecode boundary after that interval, which is why busy CPU threads thrash without ever parallelizing.

WorkloadUseWhy
I/O-boundthreading / asyncioGIL is released during blocking syscalls, so threads overlap waits; asyncio does the same on one thread without thread overhead.
CPU-bound (pure Python)ProcessPoolExecutor / free-threaded buildSeparate processes each have their own GIL → true parallelism; python3.14t drops the GIL but is opt-in.
CPU-bound (numeric)numpy / native C-extHeavy loop runs in C with the GIL released, so it scales without leaving Python.
Mixed I/O + CPUasyncio + process poolCoroutines for the I/O, loop.run_in_executor with a process pool to offload the CPU bursts.

asyncio

import asyncio, aiohttp

async def fetch(session, url):
    async with session.get(url) as r:
        return await r.text()

async def main(urls):
    async with aiohttp.ClientSession() as s:
        return await asyncio.gather(*(fetch(s, u) for u in urls))

asyncio.run(main(urls))

Common Pitfalls

  • Mutable default arg: def f(x=[]) — default list created once, shared across calls. Use x=None then assign inside.
  • Late binding in closures: [lambda: i for i in range(3)] all return 2. Fix: lambda i=i: i.
  • is vs ==: is = identity, == = equality. None, True, False use is; everything else ==.
  • Dict iteration order: insertion-ordered since 3.7 (CPython 3.6). OrderedDict still useful for move_to_end + equality semantics.
  • Shallow copy: a = b[:] or list(b) copies list but not nested objects. copy.deepcopy for full.
asyncio event loop — one tick
MODULE 4

Java — Collections

Which impl, which complexity.

Complexity Reference

Java Collection complexity

*LinkedList.remove given a node ref is O(1); by index is O(n).

HashMap Internals

Backing array of buckets; each bucket is a linked list. Java 8+: when a bucket grows past 8 entries AND table > 64, it converts to a red-black tree, improving worst-case from O(n) to O(log n). Default load factor 0.75 → resize (× 2) when size > capacity × 0.75.

equals / hashCode Contract

Hash collections find a key in two steps: bucket it by hashCode(), then confirm with equals(). Override one without the other and lookups break. The contract is three rules:

  • Consistency with equals: if a.equals(b), then a.hashCode() == b.hashCode(). (The reverse need not hold — unequal objects may share a hash code.)
  • Reflexive / symmetric / transitive / non-null: equals must satisfy these; e.g. a.equals(b) iff b.equals(a).
  • Stable: repeated calls return the same result as long as the fields used in equals don't change.
record Point(int x, int y) {}              // record auto-generates equals+hashCode
var seen = new HashSet<Point>();
seen.add(new Point(1, 2));
seen.contains(new Point(1, 2));            // true — value equality works

// The classic bug: a key with default identity hashCode
class Bad { int id; }                       // no equals/hashCode override
var m = new HashMap<Bad, String>();
var k = new Bad();
m.put(k, "v");
m.get(new Bad());                           // null — different identity, different bucket

Fail-Fast Iterators

The non-concurrent collections (ArrayList, HashMap, HashSet, LinkedList, …) use fail-fast iterators: each tracks a modCount, and structurally modifying the collection during iteration (anything other than the iterator's own remove) throws ConcurrentModificationException on the next next(). It is a best-effort bug detector, not a concurrency guarantee — the name is misleading, and it can also fire single-threaded.

var list = new ArrayList<>(List.of(1, 2, 3, 4));
for (Integer n : list) {
    if (n % 2 == 0) list.remove(n);         // ConcurrentModificationException
}
// Correct: iterator.remove() or removeIf
list.removeIf(n -> n % 2 == 0);

ConcurrentHashMap

var m = new ConcurrentHashMap<String, AtomicInteger>();
m.computeIfAbsent("k", k -> new AtomicInteger()).incrementAndGet();
m.merge("k", 1, Integer::sum);

Lock-striped; iteration is weakly consistent (no ConcurrentModificationException). Prefer over Collections.synchronizedMap.

PriorityQueue

var pq = new PriorityQueue<int[]>((a,b) -> a[0] - b[0]);
pq.offer(new int[]{3, 'x'});
pq.peek(); pq.poll();
// max-heap: reverse comparator
var maxPq = new PriorityQueue<Integer>(Comparator.reverseOrder());
MODULE 5

Java — Streams & Concurrency

Declarative pipelines; thread pools; CompletableFuture.

Stream API

import static java.util.stream.Collectors.*;
var byDept = employees.stream()
    .filter(e -> e.salary() > 50_000)
    .collect(groupingBy(Employee::dept, counting()));

// reduce
int sum = nums.stream().mapToInt(Integer::intValue).sum();

// parallel — only for independent, CPU-bound, large work
int total = bigList.parallelStream().mapToInt(this::score).sum();

Optional

Optional<User> u = repo.find(id);
u.map(User::email).orElse("unknown");
// Don't do .get() without isPresent — misses the point.
// Don't use Optional for fields/params — only return types.

ExecutorService

var pool = Executors.newFixedThreadPool(8);
Future<Integer> f = pool.submit(() -> heavy());
int r = f.get();
pool.shutdown();
// Java 21 virtual threads
try (var es = Executors.newVirtualThreadPerTaskExecutor()) {
    es.submit(task);
}

CompletableFuture

CompletableFuture.supplyAsync(this::loadUser)
    .thenApply(User::email)
    .thenCompose(this::sendWelcome)
    .exceptionally(ex -> { log(ex); return null; });

var all = CompletableFuture.allOf(f1, f2, f3);
var any = CompletableFuture.anyOf(f1, f2);

Locks & Concurrency Utilities

  • synchronized + Object.wait/notify — low-level, avoid for new code.
  • ReentrantLock, ReadWriteLock — fairness options, tryLock with timeout.
  • Semaphore, CountDownLatch, CyclicBarrier, Phaser.
  • AtomicInteger, LongAdder — lock-free counters.
  • StampedLock — optimistic read + write locks.
MODULE 6

Java — Interview Patterns

Comparators, resources, records, switch.

Comparator idioms

Comparator<Person> byAgeThenName =
    Comparator.comparingInt(Person::age).thenComparing(Person::name);

// reverse
list.sort(Comparator.reverseOrder());
list.sort(byAgeThenName.reversed());

// nulls last
Comparator.nullsLast(Comparator.naturalOrder());

try-with-resources

try (var in = Files.newBufferedReader(p);
     var out = Files.newBufferedWriter(dst)) {
    out.write(in.readLine());
}  // closed in reverse order, exceptions suppressed-attached

Records (Java 16+)

public record Point(int x, int y) {
    public Point { if (x < 0 || y < 0) throw new IllegalArgumentException(); }  // compact ctor
    public int sum() { return x + y; }
}
// getters, equals, hashCode, toString auto-generated; final + immutable

Pattern Matching in switch (Java 21)

String describe(Object o) {
    return switch (o) {
        case Integer i when i < 0 -> "neg int";
        case Integer i -> "int " + i;
        case String s -> "string len=" + s.length();
        case null -> "null";
        default -> "other";
    };
}
MODULE 7

C++ — STL & Modern

Containers, algorithms, move semantics, smart pointers.

Container Complexity

C++ STL container complexity

* With iterator in hand.

<algorithm>

#include <algorithm>
#include <numeric>
std::sort(v.begin(), v.end());
std::sort(v.begin(), v.end(), std::greater<>{});
auto it = std::lower_bound(v.begin(), v.end(), x);
std::accumulate(v.begin(), v.end(), 0LL);
int cnt = std::count_if(v.begin(), v.end(), [](int x){ return x>0; });
std::unique(v.begin(), v.end());   // requires sorted first; returns new end
v.erase(std::unique(v.begin(), v.end()), v.end());  // erase-remove idiom

Move Semantics & RAII

std::vector<std::string> make() {
    std::vector<std::string> v;
    v.push_back(std::string(1000, 'x'));  // move from temporary
    return v;  // NRVO / move, not copy
}

class Buf {
    std::vector<char> data;
public:
    Buf(Buf&& o) noexcept : data(std::move(o.data)) {}   // move ctor
    Buf& operator=(Buf&& o) noexcept { data = std::move(o.data); return *this; }
};

RAII: resource lifetime = object lifetime. Acquire in ctor, release in dtor. No manual new/delete in modern C++.

Smart Pointers

auto p = std::make_unique<Widget>(arg);   // sole owner
auto s = std::make_shared<Widget>(arg);   // shared owner, atomic refcount
std::weak_ptr<Widget> w = s;              // non-owning observer
MODULE 8

Go — For Interviews

Slices, maps, goroutines, channels, context.

Slices (len vs cap)

s := make([]int, 0, 10)   // len=0 cap=10
s = append(s, 1, 2, 3)    // len=3 cap=10, no realloc
fmt.Println(len(s), cap(s))

// slicing shares backing array
a := []int{1,2,3,4,5}
b := a[1:3]               // b shares storage
b[0] = 99                 // mutates a[1]!
// defensive copy
c := make([]int, len(b)); copy(c, b)

Maps

m := map[string]int{}
m["a"] = 1
v, ok := m["a"]           // ok=false if missing
delete(m, "a")
// iteration order is randomized by design

Goroutines & Channels

ch := make(chan int, 3)   // buffered
go func() {
    for i := 0; i < 3; i++ { ch <- i }
    close(ch)
}()
for v := range ch { fmt.Println(v) }

// select — first ready wins
select {
case v := <-ch:        handle(v)
case <-time.After(1*time.Second): timeout()
case <-ctx.Done():     return ctx.Err()
}

sync

var (
    mu sync.Mutex
    wg sync.WaitGroup
)
for _, url := range urls {
    wg.Add(1)
    go func(u string) {
        defer wg.Done()
        r, err := http.Get(u)
        mu.Lock(); results[u] = r; mu.Unlock()
    }(url)
}
wg.Wait()

context.Context

ctx, cancel := context.WithTimeout(parent, 2*time.Second)
defer cancel()
req, _ := http.NewRequestWithContext(ctx, "GET", url, nil)

Every blocking or RPC call takes a context.Context as its first arg. Cancellation propagates automatically.

Go runtime scheduler — G/M/P model

Error handling

if err := doX(); err != nil {
    return fmt.Errorf("do X: %w", err)   // wrap
}
var nf *NotFound
if errors.As(err, &nf) { /* custom type */ }
if errors.Is(err, io.EOF) { /* sentinel */ }
MODULE 9

TypeScript — Essentials

Types, generics, narrowing, strict mode.

Types vs Interfaces

type User = { id: number; name: string };
interface User2 { id: number; name: string }

// interface: declaration merging, extends via `extends`
// type:      unions, intersections, mapped, conditional
type Role = "admin" | "user" | "guest";
type AdminUser = User & { role: "admin" };

Generics

function first<T>(xs: T[]): T | undefined { return xs[0]; }

// constraints
function getKey<T, K extends keyof T>(obj: T, k: K): T[K] { return obj[k]; }

class Cache<T> {
    private map = new Map<string, T>();
    get(k: string): T | undefined { return this.map.get(k); }
}

Utility Types

type U  = Partial<User>;           // all optional
type U2 = Required<U>;
type U3 = Readonly<User>;
type P  = Pick<User, "id" | "name">;
type O  = Omit<User, "id">;
type R  = Record<Role, User[]>;    // map Role to User[]
type T  = ReturnType<typeof fn>;
type A  = Awaited<Promise<string>>;   // string

Discriminated Unions + Narrowing

type Shape =
    | { kind: "circle"; r: number }
    | { kind: "square"; side: number };

function area(s: Shape): number {
    switch (s.kind) {
        case "circle": return Math.PI * s.r ** 2;   // s narrowed
        case "square": return s.side ** 2;
    }
}

// Type guard fn
function isErr(x: unknown): x is Error {
    return x instanceof Error;
}

Strict Mode

Enable "strict": true in tsconfig. This turns on noImplicitAny, strictNullChecks, strictFunctionTypes, and more. any defeats the type system — prefer unknown + narrowing.

TypeScript narrowing paths
MODULE 10

Language-agnostic Interview Habits

How to write good code under time pressure.

Naming

  • Full words > abbreviations. customer_count > custCnt.
  • Boolean names start with is, has, can.
  • Collections in plural. Loop variables short: i, j, k for indices, single noun for items.
  • Match the domain, not the implementation. orders > order_list.

Early Return & Guard Clauses

# prefer this
def process(req):
    if not req.valid: return Err(422)
    if not req.user.active: return Err(403)
    # happy path, flat

Deep nesting hides the happy path. Flatten with early return.

Error Handling

  • Handle errors at the right boundary. Don't catch-and-swallow deep inside.
  • Attach context when wrapping: fmt.Errorf("load user %d: %w", id, err).
  • Distinguish programmer errors (panics, assertions) from user-visible errors.

Test Skeleton

In interview, leave an empty test block — shows you think test-first. e.g. "# test: empty list returns []; list of dups returns 1; mixed returns sorted unique".

Communication

  • Talk while you think: state your current approach before coding.
  • Confirm constraints before optimizing ("is n ≤ 10⁵? Then O(n log n) is fine").
  • When stuck: enumerate what you know, what you don't. Ask for a hint if truly stuck — that's fine.
  • Before declaring done: trace through one case; state time & space.
MODULE 11

C++ Depth, Rust & Runtime Internals

The thin C++ tier, missing Rust, and the CPython internals seniors get asked.

C++ Templates, SFINAE & Concepts

Templates are compile-time code generation: the compiler stamps out a fresh function or class for every distinct set of type arguments. There is no runtime dispatch and no boxing — std::vector<int> and std::vector<double> are unrelated types with separately compiled code. This is monomorphization, the same model Rust uses for generics, and it is why C++ generic code is zero-overhead but bloats binary size and leaks into compile times.

// Generic max — instantiated per type at the call site.
template <typename T>
T max_of(T a, T b) { return a < b ? b : a; }

// Constrain with a C++20 concept: readable errors, checked at declaration.
template <typename T>
concept Ordered = requires(T a, T b) { { a < b } -> std::convertible_to<bool>; };

template <Ordered T>
T max_ok(T a, T b) { return a < b ? b : a; }

Before C++20, constraints were expressed with SFINAE — "Substitution Failure Is Not An Error." If a template argument makes the signature ill-formed, that overload is silently dropped from the candidate set rather than causing a hard error. It works but produces novel-length error messages.

// SFINAE: enable only for integral T.
template <typename T,
          typename = std::enable_if_t<std::is_integral_v<T>>>
T twice(T x) { return x + x; }

Concepts subsume SFINAE for most work: they read like a contract, participate in overload resolution, and pin the error to the call site ("constraint not satisfied") instead of deep inside the template body.

Const-Correctness & the Value Categories

const is a compile-time promise about who may mutate what. It participates in overload resolution, propagates through member functions, and is the primary tool for expressing intent in an API. Getting it right prevents whole classes of aliasing bugs and unlocks optimizations, because the compiler knows a const& parameter will not be written through.

  • Top-level constconst int x: the object is immutable. Ignored on by-value parameters for signature matching.
  • Low-level constconst int* p: pointee is immutable, pointer is not. Read declarations right-to-left: int* const is a const pointer to mutable int.
  • const member functionssize() const: may be called on const objects and promises not to modify the observable state. mutable members (e.g. a cache or mutex) are the escape hatch.
  • constexpr vs constconst means "won't change"; constexpr means "computable at compile time." Every constexpr is implicitly const, not vice versa.
struct Grid {
  int at(int i) const { return data_[i]; }   // read-only overload
  int& at(int i)      { return data_[i]; }   // mutable overload
  std::vector<int> data_;
};
const Grid g{{1,2,3}};
// g.at(0) picks the const version; assigning through it won't compile.

Iterator Invalidation & Undefined Behavior

An iterator, reference, or pointer into a container can be silently invalidated by an operation that reallocates or relinks the container's storage. Using an invalidated iterator is undefined behavior (UB) — the program may crash, corrupt data, or appear to work until it doesn't. The rules are per-container and worth memorizing for the common cases.

ContainerInvalidated on insertInvalidated on erase
vectorAll iters/refs if capacity grows; from insert point onward otherwiseFrom erase point onward
dequeAll iterators; refs stay valid only if insert at an endAll iterators and refs (unless at an end)
list / map / setNoneOnly the erased element's iterator
unordered_mapIterators invalidated on rehash; refs stay validOnly the erased element
// The classic vector footgun.
std::vector<int> v{1,2,3,4};
for (auto it = v.begin(); it != v.end(); ++it)
  if (*it % 2 == 0) v.erase(it);        // UB: it is now dangling

// Correct: erase returns the next valid iterator.
for (auto it = v.begin(); it != v.end(); )
  it = (*it % 2 == 0) ? v.erase(it) : it + 1;

// Better: erase-remove idiom (C++20: std::erase(v, ...)).
std::erase_if(v, [](int x){ return x % 2 == 0; });

UB is broader than dangling iterators: signed integer overflow, reading an uninitialized value, out-of-bounds access, data races, and null dereference are all UB. The compiler is permitted to assume UB never happens and optimize accordingly — which is how a null check after a dereference can be deleted entirely.

RAII, Move Semantics & the Vocabulary Types

RAII — Resource Acquisition Is Initialization — ties a resource's lifetime to a stack object's scope. Acquire in the constructor, release in the destructor, and the resource is freed on every exit path including exceptions. This is why idiomatic C++ needs almost no manual delete, close(), or unlock(): unique_ptr, lock_guard, and fstream do it for you.

std::mutex m;
void safe() {
  std::lock_guard<std::mutex> g(m);  // locks now
  do_work();                          // unlocks even if this throws
}                                     // ~lock_guard() releases

Move semantics let you transfer ownership of a resource instead of deep-copying it. An rvalue (a temporary or something std::moved) can be gutted: the move constructor steals the pointer and nulls the source, turning an O(n) copy into an O(1) pointer swap. This is what makes returning a large vector by value cheap.

std::vector<std::string> build();
auto data = build();                  // moved, not copied (also RVO-elided)
std::string a = "long string here...";
std::string b = std::move(a);         // b steals a's buffer; a is now empty

The vocabulary types encode intent in the type system:

  • std::string_view — a non-owning (ptr, len) window over char data. Zero-copy substring/parse. Danger: it does not extend lifetime; a view into a temporary dangles immediately.
  • std::optional<T> — "a T or nothing," without sentinels like -1 or null. Check with has_value() / if (opt); *opt when empty is UB.
  • std::variant<A,B,C> — a type-safe tagged union. Visit with std::visit; wrong-alternative access throws bad_variant_access.

Rust: Ownership, Borrowing & Lifetimes

Rust achieves C++-class performance with memory safety enforced at compile time, no garbage collector. The mechanism is the borrow checker built on three rules: every value has exactly one owner; when the owner goes out of scope the value is dropped; and you may hand out either one mutable reference or any number of shared references, never both at once (the aliasing-XOR-mutation rule). This statically prevents use-after-free, double-free, and data races.

fn main() {
    let s = String::from("hello");
    let t = s;                 // MOVE: ownership transfers to t
    // println!("{s}");        // compile error: s was moved out of

    let u = t.clone();         // explicit deep copy if you want two owners
    let len = calc_len(&u);    // BORROW: pass a reference, keep ownership
    println!("{u} is {len}");  // u still valid here
}
fn calc_len(x: &String) -> usize { x.len() }  // borrow ends at return

Lifetimes are the compiler's name for "how long a reference is valid." Usually inferred; occasionally you annotate them so the compiler can prove a returned reference does not outlive its source. Lifetimes are erased at runtime — they are purely a proof obligation, zero cost.

// 'a says: the result lives no longer than BOTH inputs.
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}

When to reach for Rust: systems work where you'd otherwise pick C++ (kernels, browsers, databases, embedded), but you want the compiler to eliminate memory-safety and data-race bugs; fearless concurrency (Send/Sync are checked); replacing hot C/C++ paths in a Python/JS service. Reach for something else when you need a huge mature library ecosystem fast, iteration speed over control (Go, Python), or when the borrow checker's learning curve outweighs the safety win for a short-lived script.

Python Internals Beyond the GIL

Every CPython value is a heap-allocated PyObject carrying a reference count and a type pointer. Memory is reclaimed primarily by reference counting: each new binding increments the count, each del or rebinding decrements it, and reaching zero frees the object immediately (deterministic, unlike a tracing GC). A separate cyclic garbage collector exists solely to break reference cycles that refcounting alone can never reach.

import sys
a = []
b = a               # two names, one object
print(sys.getrefcount(a))  # 3: a, b, and the temp arg to getrefcount

# Reference cycle: refcounts never hit 0 on their own.
x = {}; y = {}
x['y'] = y; y['x'] = x     # x->y->x; only the cyclic GC can collect these

Two traps that recur in real code and interviews:

  • __slots__ — By default each instance carries a __dict__, a full hash table. Declaring __slots__ = ('x','y') replaces it with fixed slots: no per-instance dict, faster attribute access, and often 40-50% less memory per object. Cost: no ad-hoc attributes and no default __weakref__/dict-based tricks.
  • Mutable default argument — A default value is evaluated once at function-definition time and shared across all calls. A mutable default (list/dict) accumulates state between calls.
def bad(item, bucket=[]):      # bucket created ONCE, shared forever
    bucket.append(item); return bucket
bad(1)          # [1]
bad(2)          # [1, 2]  <- surprise, same list

def good(item, bucket=None):   # sentinel idiom
    if bucket is None: bucket = []
    bucket.append(item); return bucket
class Point:                   # __slots__ demo
    __slots__ = ('x', 'y')
    def __init__(self, x, y): self.x, self.y = x, y
p = Point(1, 2)
# p.z = 3  -> AttributeError: 'Point' object has no attribute 'z'

Cross-Cutting Numeric Correctness

Numeric bugs are language-portable landmines: the same off-by-a-bit mistake surfaces in C++, Java, Go, and Python differently because each language chose different rules for width, overflow, and constant evaluation. Know the model, not just the syntax.

ConcernBehaviorConsequence
C++ signed overflowUndefined behaviorCompiler may assume it can't happen; loops get miscompiled
C++/Java unsigned / int wrapWell-defined modulo 2ⁿ0u - 1 == 4294967295; wraps silently
Java int vs long32-bit vs 64-bit, both signed, both wrapint maxes at ~2.1e9; sums of counts overflow
Python intArbitrary precision (bignum)Never overflows; but crossing to C/NumPy re-introduces fixed width
Go untyped constantsArbitrary precision until assigned a typeconst Big = 1 << 62 is fine; overflow checked at conversion
// Java: int overflows at ~2.1 billion. Force 64-bit BEFORE the multiply.
int  a = 100000, b = 100000;
long bad  = a * b;          // 10,000,000,000 wanted; int math wraps to 1410065408
long good = (long) a * b;   // promote first -> correct 10,000,000,000

IEEE-754 binary floating point cannot represent most decimal fractions exactly. 0.1 + 0.2 != 0.3 in every IEEE-754 language because 0.1 has no finite binary expansion. Rules of engagement: never test floats with == (use an epsilon or ULP comparison); never store money as double (use integer cents or a decimal type); and remember NaN != NaN, which breaks naive sorting and set membership.

print(0.1 + 0.2)              # 0.30000000000000004
print(0.1 + 0.2 == 0.3)      # False
import math
print(math.isclose(0.1 + 0.2, 0.3, rel_tol=1e-9))  # True

A Language-Selection Guide

There is no "best" language, only a best fit for a constraint set: latency budget, safety requirements, team skills, ecosystem, and how much control over memory you actually need. Map the dominant constraint to the tool.

If the dominant need is…Reach forBecause
Max control, zero-overhead abstraction, legacy interopC++Deterministic RAII, templates, huge existing systems; you accept UB risk
Systems performance + memory/thread safety guaranteedRustBorrow checker eliminates whole bug classes at compile time, no GC
Fast iteration, data/ML, glue, scriptingPythonVast ecosystem, readable; accept GIL + interpreter overhead
Simple concurrent network services, fast buildsGoGoroutines, one static binary, minimal footguns; less expressive generics
Enterprise/Android, mature tooling & JITJava/KotlinManaged GC, strong libraries, predictable throughput at scale
  1. Start from the hard constraint. A 100µs tail-latency SLO or a 4KB microcontroller rules out GC languages before any other consideration.
  2. Then weigh safety vs. speed-to-market. Rust's compile-time guarantees pay off over a service's lifetime; Python/Go win the first ninety days.
  3. Then ecosystem and team. The library you don't have to write and the language your team already knows are real, measurable advantages.
  4. Polyglot is normal. Python orchestration calling a Rust or C++ extension for the hot loop is often the correct answer, not a compromise.