2D Vector Class

An immutable value type with operators

Design an immutable 2D vector with natural operator syntax, dot and cross products, rotation, and equality and hashing that agree with each other.

Overview

A 2D vector is a pair of numbers, x and y, that stands for a position, a velocity or a direction on a plane. Games, physics simulations and drawing code create millions of them. The class looks trivial, which is why it is a good interview problem: it tests whether you know how a value type should behave, how Python's operators are wired, and how floating-point numbers break naive equality.

The two decisions that matter are immutability and equality. Immutability means every operation returns a new vector and nothing can change an existing one; that makes vectors safe to share and safe to use as dict keys. Equality must agree with hashing: two vectors that compare equal must hash the same, so the tolerant comparison many people want in eq has to live somewhere else.

A strong answer says 'value type, immutable, exact equality plus a separate tolerant compare' in the first minute, then writes the dunder methods with NotImplemented for unsupported operands.

Scoping the problem

  • Are vectors mutable? — Decides whether they can be dict keys and shared safely — Immutable
  • Which operators: plus, minus, scalar times, divide, negate, abs? — Each is one dunder method — All of these; no vector times vector with *
  • Do we need vector times scalar in both orders? — 2 * v needs rmul — Yes
  • Exact or tolerant equality? — Tolerant eq breaks hashing — Exact eq; a separate is_close
  • Radians or degrees? — Every angle method must agree — Radians, like math
  • Integers, floats, or both? — Mixed input is normal; store floats — Accept both, store as given

Core entities

There is one class. The interview is about its contract, not about a class hierarchy.

Why immutable

A mutable vector shared between two objects is the classic bug: move one and the other moves too. An immutable vector cannot be changed, so sharing is always safe, and the object can be a dict key or a set member because its hash never changes.

  • Sharing — dangerous; copy defensively — free
  • Dict key or set member — unsafe; the hash can change — safe
  • Threads — needs a lock or a copy — safe with no lock
  • Cost — in-place updates, no allocation — one new object per operation
  • Fits — tight inner loops in a physics engine — almost everything else

Python's @dataclass(frozen=True) does the work: assignment to a field raises FrozenInstanceError, and with eq=True (the default) and frozen=True the decorator also generates hash from the fields.

Operators

Python maps each operator to a specially named method. Return NotImplemented, not False and not an exception, when the other operand is a type you do not handle: Python then tries the other operand's reflected method and only raises TypeError if nothing works.

  • a + b — a.add(b) — new vector
  • a - b — a.sub(b) — new vector
  • a * 2 — a.mul(2) — new vector
  • 2 * a — int.mul gives NotImplemented, then a.rmul(2) — new vector
  • a / 2 — a.truediv(2) — new vector
  • -a — a.neg() — new vector
  • abs(a) — a.abs() — the length, a float
  • a == b — a.eq(b), generated by the dataclass — exact, field by field
  • a * b — mul returns NotImplemented — TypeError: use dot or cross

Equality and hashing

Every floating-point operation rounds its result: 0.1 + 0.2 is not 0.3 in binary floating point, and two vectors computed by different routes can differ in the last bit. The temptation is to write a tolerant eq. Do not: Python requires that objects which compare equal have the same hash, and no hash function can give equal values to every pair of numbers that are merely close.

  • Exact eq plus is_close() (this design) — exact — consistent — work — call is_close
  • Tolerant eq, no hash — tolerant — none: hash is set to None — TypeError — work
  • Tolerant eq plus rounded hash — tolerant, not transitive — breaks near a rounding boundary — silent misses — work

math.isclose uses a relative tolerance by default (rel_tol=1e-09, abs_tol=0.0). Comparing to zero needs an absolute tolerance, because a relative tolerance of zero is zero. is_close in the code passes both through.

What the products mean

  • dot(a, b) — ax·bx + ay·by — length of a times length of b times cos θ; zero when perpendicular; sign says same or opposite direction — projections, lighting, angle between
  • cross(a, b) — ax·by − ay·bx — the z part of the 3D cross product; signed area of the parallelogram — which side of a line a point is on; turning direction
  • abs(a) — hypot(x, y) — length — speed, distance
  • angle() — atan2(y, x) — angle from the positive x axis, between −π and π — headings; atan2 knows the quadrant, atan(y/x) does not
  • rotated(θ) — (x cos θ − y sin θ, x sin θ + y cos θ) — counterclockwise turn by θ — steering, drawing

The rotation is the 2×2 rotation matrix applied to the column (x, y). Rotating (1, 0) by π/2 gives (0, 1): the x axis turns onto the y axis. The demo asserts that.

What you will be able to do

  • Write a frozen dataclass vector whose operators feel like arithmetic and reject bad operands cleanly.
  • Explain why equality must be exact when hashing exists, and where a tolerant compare belongs.
  • Say what dot and cross mean geometrically and use them for angle and side tests.
  • Handle the zero vector in normalize and angle without producing NaN.
  • Answer the follow-ups: 3D, performance in tight loops, and integer versus float components.

Functional requirements

  • Add and subtract vectors; multiply and divide by a scalar in either order; negate
  • Length via abs(), normalization to a unit vector, distance between two points
  • Dot product, 2D cross product (a scalar), angle from the x axis and angle between two vectors
  • Rotation by an angle in radians, counterclockwise
  • Factories: zero(), unit_x(), unit_y(), from_angle(radians, length)
  • Exact equality and hashing, plus is_close() with tolerances for geometry tests

Non-functional requirements

  • Immutable: no method changes a vector; assignment to x or y raises
  • Usable as dict keys and set members: equal vectors hash equal
  • Unsupported operands return NotImplemented so Python raises a clear TypeError
  • The zero vector never produces NaN: normalized() and angle_to() raise ValueError instead
  • Small memory footprint: slots, two floats per instance

Interview questions

  • Why not compare with a tolerance in eq? It would make the rotation test pass with ==.
  • How would you add a 3D vector without duplicating everything?
  • A physics loop creates a new vector for every operation. Is that a problem?
  • Why do dot and cross have names while add and multiply use operators?
  • What should the zero vector do in normalize and angle?

Sign up free to read the full topic

More Low-Level Design topics

Start free on Cariara · Coding problems · System design studio