skip to content

How does the compiler translate x in range and x !in range, and why does that let you use in on non-range types?

level: middleimportance: should knowfreq 50%

answer

  1. x in y -> y.contains(x); operands swap
  2. x !in y -> !y.contains(x)
  3. Any operator fun contains type works with in
  4. Range contains is O(1) boundary check, not a scan
  5. for-in needs iterator(); membership-in needs contains

basics

~10 s

x in y becomes y.contains(x), and x !in y becomes !y.contains(x). Any type that defines a contains function works with in, not just ranges — lists, sets, and strings do too.

solid answer

~50 s

`in` is a **convention-based operator**: the compiler rewrites `x in y` to `y.contains(x)` and `x !in y` to `!y.contains(x)`. The receiver is the right-hand operand `y`, the argument is the left-hand `x`. `contains` must be marked `operator fun contains(...)`. Ranges define it (`IntRange.contains`), which is why `5 in 1..10` works, but so do `Collection` (`x in list`), `Set`, `Map.keys`, `CharSequence`/`String` (substring/char check), and any custom type you write. Because it is convention-based, you can add membership semantics to your own class by declaring an `operator fun contains`. For ranges of comparables, `contains` is typically an O(1) boundary comparison (`x >= start && x <= endInclusive`) rather than a scan, so `in` on a numeric range is cheap and does not iterate. The same `in` token means iteration in a `for` header — a separate convention requiring `iterator()`.

code

kotlin · 6 lines
kotlin
operator fun IntRange.contains(s: String) = s.length in this
println("hi" in 1..3)   // true: "hi".length == 2, 2 in 1..3

// stdlib desugaring examples
println(5 in listOf(1,5,9))  // List.contains -> true
println('c' in "cat")        // CharSequence.contains -> true

go deeper

for a junior

Knows in tests membership and works on lists and strings too.

for a middle

States the exact y.contains(x) desugaring and operand swap, and that any contains-defining type qualifies.

for a senior

Explains O(1) range contains vs O(n) list scan and distinguishes the two in conventions; adds in to custom types.

for a principal

Designs membership APIs deliberately — extension operator contains, complexity guarantees, and avoiding surprising semantics.

## The desugaring rule Kotlin's `in` operator is defined by **convention**, not hardwired to ranges. The compiler translates: - `x in y` -> `y.contains(x)` - `x !in y` -> `!y.contains(x)` Note the operands swap roles: the thing on the **right** (`y`) is the receiver, and the thing on the **left** (`x`) is the argument passed to `contains`. ## What makes a type usable with in Any type that declares an `operator fun contains` works: ```kotlin class DateRange(val start: LocalDate, val end: LocalDate) { operator fun contains(d: LocalDate): Boolean = d >= start && d <= end } val q1 = DateRange(LocalDate.of(2026, 1, 1), LocalDate.of(2026, 3, 31)) val inQ1 = LocalDate.of(2026, 2, 14) in q1 // -> q1.contains(...) ``` Standard-library types already define it: ```kotlin 5 in listOf(1, 5, 9) // List.contains 'a' in "cat" // CharSequence.contains -> char present "at" in "cat" // String.contains -> substring present key in someMap // Map.containsKey via keys ``` ## Why ranges are fast For a comparable range, `contains` is an **O(1) boundary check**, not a scan: ```kotlin // conceptually for IntRange: operator fun contains(value: Int) = value >= first && value <= last ``` So `1_000_000 in 1..2_000_000` compares two bounds; it does **not** walk a million numbers. Contrast `x in someList`, which is O(n) because a list must scan. ## Membership in vs iteration in The `in` keyword has two distinct roles, resolved by position: ```kotlin if (x in range) { ... } // boolean position -> contains for (x in range) { ... } // for header -> iterator() ``` In a `for` header, `in` requires an `iterator()` operator function (which `Iterable`, and therefore `IntRange`, provides). In a boolean/expression position it requires `contains`. They are independent conventions that happen to share the keyword. ## extension contains `contains` can also be an **extension** operator function, letting you add `in` support to types you don't own: ```kotlin operator fun ClosedRange<Int>.contains(strings: List<Int>) = strings.all { it in this } ``` ## Key APIs/keywords - `operator fun contains` (the convention) - `in` (membership) vs `in` (iteration -> `iterator()`) - `Collection.contains`, `CharSequence.contains`, `Map.containsKey` - `ClosedRange.contains` (O(1) for comparables)

  • Which operand is the receiver in x in y?
    The right-hand one, y. It compiles to y.contains(x), so y is the receiver and x the argument.
  • Is 5_000_000 in 1..10_000_000 expensive?
    No. Range contains is an O(1) comparison of value against the bounds; it does not iterate the range.

in is a question you hand to the container: 'do you contain this?' — every container that knows how to answer (contains) can be asked.

saying these in an interview costs you the question

  • Saying x in y compiles to x.contains(y) (operands reversed)
  • Believing in only works on ranges and collections
  • Claiming range membership iterates / is O(n)
  • Not knowing contains can be an extension operator
  • Conflating the for-loop in (iterator) with membership in (contains)

context