1. Language Fundamentals & Memory Model

Covers integer overflow, float precision, equality semantics, hashCode/equals contracts, copy semantics, and memory-management mechanics.

Before you write a single algorithm, your language is already making decisions about how numbers overflow, how equality is checked, and how memory gets allocated and reclaimed. This section is a mechanics reference — how Python, Java, Go, and JavaScript actually represent and manage data under the hood — so those decisions never surprise you mid-interview.

Language Verdict: Pros, Cons & Recommendation

Python

4/5
  • Arbitrary-precision int eliminates overflow bugs entirely — no long promotion to remember
  • Simple, consistent equality model: == for value, is reserved for identity/None checks
  • @dataclass(frozen=True) gives a correct __hash__/__eq__ pair for custom keys in one line
  • Deterministic refcounting GC frees most objects instantly, easier to reason about than generational pauses
  • Dynamic typing means wrong-type bugs (e.g. add("a", "b")) surface only at runtime, not compile time
  • Every int/float is a full heap object (~28 bytes) — no unboxed primitive, which hurts tight numeric loops

Java

5/5
  • Static typing plus generics catch wrong-type arguments at compile time, before the code ever runs
  • record (Java 16+) auto-generates a correct equals()/hashCode() pair, closing off the classic contract bug
  • Primitive types (int, long, ...) give zero-overhead, unboxed numerics alongside boxed wrappers when needed
  • BigInteger/BigDecimal cover arbitrary-precision and exact-decimal needs explicitly
  • int silently wraps on overflow with no exception — a frequent, hard-to-spot bug source
  • A stray null dereference throwing NullPointerException is the single most common runtime crash
  • Boxed Integer cache (-128..127) makes == "work" for small values and silently break outside it

Go

4/5
  • Static types catch wrong-argument bugs at compile time — no add("a", "b") surprise
  • Unboxed int/int64/float64 in slices — no per-element heap object like Python or boxed Integer
  • Zero values (0, "", nil) make absence explicit; comma-ok on maps disambiguates missing vs zero
  • == is value equality for comparable types (including structs of comparables) — no .equals contract to get wrong
  • int is 32- or 64-bit depending on GOARCH; overflow on int32/int64/uint wraps silently with no panic
  • No language-level exceptions — errors are values (easy to ignore) and true crashes are panic
  • Slices, maps, and funcs are not comparable (except to nil) — no structural ==, reach for slices.Equal / maps.Equal / reflect.DeepEqual

JavaScript

3/5
  • Single number type keeps arithmetic simple — no int/float or primitive/boxed split to reason about
  • Object.is/=== are well-defined once you know to avoid =='s coercion
  • BigInt covers arbitrary-precision integers when a problem's numbers exceed 2^53
  • structuredClone/spread give straightforward copy semantics with no extra imports
  • All numbers are doubles — integers silently lose precision past Number.MAX_SAFE_INTEGER (2^53 - 1), with no error
  • == and + perform aggressive silent type coercion, producing wrong-typed results instead of throwing
  • Two distinct absence values (null and undefined) with subtly different semantics to track
  • Map/Set have no hook for custom equality/hashing on object keys — must serialize to a primitive
Recommendation: Python remains the default DSA pick — unbounded int and simple == remove two entire bug classes. Java and Go both give compile-time types and unboxed numerics, but both wrap on overflow (Go's int is also platform-width, 32- or 64-bit). Treat JS's coercion and 2^53 precision limit as the biggest correctness risk of the four.

Coding Mechanics, Side by Side

Integer Representation & Overflow

Must-know

This is the single most common silent-bug source when porting a solution between languages: only Python's int is unbounded — Go, like Java, wraps on overflow.

x = 2 ** 62 x = x * x * x # keeps growing, no overflow print(x) # CPython transparently promotes to arbitrary-precision ints; # there is no separate "long" type in Python 3.

Python ints have arbitrary precision — they grow as needed and never silently wrap around. The cost is that big-int arithmetic is slower than fixed-width math and each int is a heap-allocated object, not a raw machine word. In interviews this means overflow is never a real concern in Python, but you should still call it out verbally if the problem is language-agnostic (interviewer may be judging Java/C++ intuition).

Floating-Point Precision

Must-know

All four languages use IEEE-754 64-bit doubles for their default float type (float / double / float64 / number), so the classic rounding gotcha is identical everywhere.

print(0.1 + 0.2 == 0.3) # False print(round(0.1 + 0.2, 10) == 0.3) # True from decimal import Decimal Decimal('0.1') + Decimal('0.2') # Decimal('0.3'), exact from fractions import Fraction Fraction(1, 3) + Fraction(1, 6) # Fraction(1, 2), exact

float is a 64-bit double, same binary representation issues as everywhere else. For interview purposes, comparing floats with == is a smell — use a tolerance (abs(a - b) < 1e-9) or math.isclose. Decimal and Fraction exist for exact arithmetic but are rarely expected in a DSA interview unless the problem explicitly involves currency or exact rational results.

Equality: Value vs. Identity

Must-know
a = [1, 2, 3] b = [1, 2, 3] print(a == b) # True (value equality, via __eq__) print(a is b) # False (different objects) x = 256; y = 256 print(x is y) # True (small-int cache, CPython impl detail) x = 257; y = 257 print(x is y) # False (usually — not guaranteed, don't rely on it!)

== calls __eq__ (value equality, customizable); is checks object identity (same memory address). Never use is for value comparison — the small-int cache (-5..256) makes is appear to "work" for small numbers, which is a trap, not a guarantee. Always use == for values, is only for None/sentinel checks.

hashCode/equals Contract for Custom Map/Set Keys

Must-know

Using a custom object as a hash map/set key requires deliberate setup in every language — none of them "just work" out of the box. Go is the exception for structs of comparable fields: those are valid map keys with no extra method.

from dataclasses import dataclass @dataclass(frozen=True) class Point: x: int y: int # __eq__ and __hash__ auto-generated from fields, consistent by construction seen = {Point(1, 2): "a"} print(Point(1, 2) in seen) # True

Default object __hash__ is identity-based (id()), and default __eq__ is also identity-based — so two "equal-looking" plain objects won't match as dict keys unless you override both. @dataclass(frozen=True) auto-generates a correct, consistent __eq__/__hash__ pair for you. If you override __eq__ manually without __hash__, Python sets the class's __hash__ to None, making instances unhashable (fails loudly, at least).

Argument Passing Semantics

Recommended
def try_reassign(lst): lst = [9, 9, 9] # rebinds local name only, caller unaffected def mutate(lst): lst.append(4) # mutates the shared object, caller SEES this a = [1, 2, 3] try_reassign(a); print(a) # [1, 2, 3] mutate(a); print(a) # [1, 2, 3, 4]

Python passes object references "by value" — often called call-by-object-sharing. Reassigning the parameter inside the function only rebinds the local name and never affects the caller's variable. But calling a mutating method on that same object (append, [i] =, ...) is visible to the caller, since both names point at the same heap object. Ints/strings/tuples are immutable, so mutation-based aliasing bugs are only a risk with lists/dicts/sets/custom objects.

Copying: Shallow vs. Deep

Recommended
import copy orig = [[1, 2], [3, 4]] shallow = orig[:] # or list(orig) — copies outer list only shallow[0].append(99) # mutates orig[0] too! deep = copy.deepcopy(orig) # fully independent nested structure

[:], list(x), and .copy() all create a new outer container but keep references to the same nested objects — mutating a nested list still affects the "original". copy.deepcopy recursively clones everything, at real cost (time, memory) and can throw or need special handling for objects with custom __deepcopy__. For a flat list of immutables (ints/strings), shallow copy is already effectively a deep copy.

Boxing/Unboxing & Numeric Overhead

Recommended
import sys print(sys.getsizeof(1)) # 28 bytes, even for a tiny int print(sys.getsizeof(10**30)) # bigger — grows with magnitude # CPython caches small ints [-5, 256] as singletons, # but every int is still a full heap object — no unboxed "primitive" int exists.

Every Python number is a full heap-allocated object with type info and refcount overhead (~28 bytes for a small int) — there's no "primitive" int at the language level at all, so there's no boxing/unboxing distinction to reason about. This is simply the fixed cost of doing numeric work in Python; it's part of why tight numeric loops are slower than Java/C++ and why libraries like NumPy exist (packed C arrays, bypassing per-element object overhead).

GC & Memory Model Basics

Recommended
import gc class Node: def __init__(self): self.next = None self.prev = None a, b = Node(), Node() a.next, b.prev = b, a # reference cycle del a, b # refcounts don't reach 0 due to the cycle... gc.collect() # ...but the generational cycle collector still finds and frees it

CPython primarily uses reference counting — an object is freed the instant its refcount hits zero, which is deterministic and immediate (unlike Java/JS). Reference cycles (e.g., doubly linked lists, parent/child pointers) can't reach refcount zero on their own, so a separate generational cycle-detecting GC runs periodically to reclaim them — correct, but not instant, so a cycle can survive briefly after going out of scope. This rarely matters for interview-scale code but explains why implementing a doubly linked list "leaks" nothing, just isn't freed the microsecond you expect.

Big-O of Common Built-in Operations

Must-know

These complexities are the same conceptually across Python, Java, Go, and JavaScript because all four use comparable underlying structures (contiguous dynamic arrays; hash tables):

  • Array/list/slice index access (arr[i], list.get(i), s[i], arr[i]): O(1) — all are contiguous, random-access buffers.
  • Dynamic array append (list.append, ArrayList.add, append(s, x), arr.push): amortized O(1) — occasional O(n) resize-and-copy when capacity is exceeded (typically doubling / growing the backing buffer). Go's append may or may not reallocate depending on cap.
  • Dynamic array insert/delete at front or middle: O(n) — every element after the insertion point shifts.
  • Hash map get/put (dict[k], HashMap.get/put, m[k], Map.get/set): average O(1), worst case O(n) with pathological hash collisions (Python and Java both mitigate this differently — Java 8+ converts a bucket to a balanced tree once it's densely collided; Go's runtime map randomizes iteration order).
  • Hash set membership (in, .contains, comma-ok on a map[K]struct{}, .has): same as map get — average O(1).
  • Sorting (sorted/.sort(), Collections.sort/Arrays.sort, sort.Slice/slices.Sort, .sort()): O(n log n) comparison-based sort in all four (Python's Timsort, Java's Timsort-for-objects/dual-pivot-quicksort-for-primitives, Go's pdqsort, V8's TimSort as of recent versions).

What differs is language-specific, not conceptual, and is covered in the relevant compare blocks above: Java's TreeMap/TreeSet give O(log n) operations via a red-black tree (no direct built-in equivalent in Python/Go/JS without a library), and insertion-order guarantees vary (dict/Map preserve insertion order in modern Python/JS; Go map iteration is deliberately randomized; HashMap makes no such guarantee, use LinkedHashMap for that).

When You Actually Need Exact/Arbitrary-Precision Types

Optional
from decimal import Decimal, getcontext getcontext().prec = 50 print(Decimal(1) / Decimal(3)) # exact to 50 significant digits from fractions import Fraction print(Fraction(2, 4)) # Fraction(1, 2) — auto-reduced

Decimal is for exact base-10 arithmetic (money, exact rounding rules); Fraction is for exact rational numbers (auto-reduces to lowest terms). Both are rarely required in a DSA interview unless the prompt explicitly calls for exact rational/decimal results (e.g., "return the answer as a fraction in lowest terms") — know they exist so you don't reinvent them with floats.

Static vs. Dynamic Typing: Practical Interview Impact

Optional
def add(a, b): return a + b add(1, 2) # 3 add("a", "b") # "ab" — same function, no type declarations anywhere add([1], [2]) # [1, 2]

Duck-typed and dynamically typed: no compile step catches type mistakes, so a wrong-type bug (e.g., accidentally summing strings) surfaces only at runtime, sometimes deep into execution. Type hints (def add(a: int, b: int) -> int) are optional documentation only — they are not enforced at runtime and won't save you in an interview setting unless you mentally self-check types.

Null/None/Undefined Handling

Optional
x = None print(x is None) # correct idiom — never `x == None` print(bool(None)) # False d = {} print(d.get("missing")) # None, no exception print(d["missing"]) # KeyError!

There is exactly one "absence" value, None, a singleton — always compare with is None, not ==, since is is guaranteed correct and slightly faster (no __eq__ dispatch). Dict access via [] raises KeyError on a missing key while .get() returns None (or a supplied default) — pick deliberately based on whether a missing key is an error condition in your algorithm.

Further Reading