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 — 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 ""
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.
| Workload | Use | Why |
|---|---|---|
| I/O-bound | threading / asyncio | GIL 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 build | Separate processes each have their own GIL → true parallelism; python3.14t drops the GIL but is opt-in. |
| CPU-bound (numeric) | numpy / native C-ext | Heavy loop runs in C with the GIL released, so it scales without leaving Python. |
| Mixed I/O + CPU | asyncio + process pool | Coroutines 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. Usex=Nonethen assign inside. - Late binding in closures:
[lambda: i for i in range(3)]all return 2. Fix:lambda i=i: i. isvs==:is= identity,=== equality.None,True,Falseuseis; 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[:]orlist(b)copies list but not nested objects.copy.deepcopyfor full.
Java — Collections
Which impl, which complexity.
Complexity Reference
*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), thena.hashCode() == b.hashCode(). (The reverse need not hold — unequal objects may share a hash code.) - Reflexive / symmetric / transitive / non-null:
equalsmust satisfy these; e.g.a.equals(b)iffb.equals(a). - Stable: repeated calls return the same result as long as the fields used in
equalsdon'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());
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.
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";
};
}
C++ — STL & Modern
Containers, algorithms, move semantics, smart pointers.
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
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.
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 */ }
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.
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, kfor 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.
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 const —
const int x: the object is immutable. Ignored on by-value parameters for signature matching. - Low-level const —
const int* p: pointee is immutable, pointer is not. Read declarations right-to-left:int* constis a const pointer to mutable int. - const member functions —
size() const: may be called on const objects and promises not to modify the observable state.mutablemembers (e.g. a cache or mutex) are the escape hatch. - constexpr vs const —
constmeans "won't change";constexprmeans "computable at compile time." Everyconstexpris 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.
| Container | Invalidated on insert | Invalidated on erase |
|---|---|---|
vector | All iters/refs if capacity grows; from insert point onward otherwise | From erase point onward |
deque | All iterators; refs stay valid only if insert at an end | All iterators and refs (unless at an end) |
list / map / set | None | Only the erased element's iterator |
unordered_map | Iterators invalidated on rehash; refs stay valid | Only 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
-1or null. Check withhas_value()/if (opt);*optwhen empty is UB. - std::variant<A,B,C> — a type-safe tagged union. Visit with
std::visit; wrong-alternative access throwsbad_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.
| Concern | Behavior | Consequence |
|---|---|---|
| C++ signed overflow | Undefined behavior | Compiler may assume it can't happen; loops get miscompiled |
| C++/Java unsigned / int wrap | Well-defined modulo 2ⁿ | 0u - 1 == 4294967295; wraps silently |
Java int vs long | 32-bit vs 64-bit, both signed, both wrap | int maxes at ~2.1e9; sums of counts overflow |
Python int | Arbitrary precision (bignum) | Never overflows; but crossing to C/NumPy re-introduces fixed width |
| Go untyped constants | Arbitrary precision until assigned a type | const 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 for | Because |
|---|---|---|
| Max control, zero-overhead abstraction, legacy interop | C++ | Deterministic RAII, templates, huge existing systems; you accept UB risk |
| Systems performance + memory/thread safety guaranteed | Rust | Borrow checker eliminates whole bug classes at compile time, no GC |
| Fast iteration, data/ML, glue, scripting | Python | Vast ecosystem, readable; accept GIL + interpreter overhead |
| Simple concurrent network services, fast builds | Go | Goroutines, one static binary, minimal footguns; less expressive generics |
| Enterprise/Android, mature tooling & JIT | Java/Kotlin | Managed GC, strong libraries, predictable throughput at scale |
- Start from the hard constraint. A 100µs tail-latency SLO or a 4KB microcontroller rules out GC languages before any other consideration.
- 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.
- Then ecosystem and team. The library you don't have to write and the language your team already knows are real, measurable advantages.
- Polyglot is normal. Python orchestration calling a Rust or C++ extension for the hot loop is often the correct answer, not a compromise.