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
- 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
- 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
- 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
- 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 coercionBigInt covers arbitrary-precision integers when a problem's numbers exceed 2^53structuredClone/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
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).
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.
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.
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).
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.
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.
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).
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.
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).
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.
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.
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.