Kotlin vs Python Interview Questions
Reviewed by Mark Dickie · Last updated
Kotlin and Python are both general-purpose, statically and dynamically typed languages respectively, differing mainly in their type systems, execution models, and primary ecosystems. Kotlin targets the JVM and leans into null-safety, coroutines, and Android development, while Python emphasizes readable syntax, a massive standard library, and dominance in data and scripting. For interview prep, Kotlin questions tend to test type theory, concurrency with coroutines, and interoperability with Java, whereas Python questions focus on idiomatic patterns, data structures, and gotchas around mutability and scoping.
| Dimension | Kotlin | Python |
|---|---|---|
| Type system | Static, inferred, null-safe | Dynamic, duck-typed |
| Primary ecosystem | JVM, Android, server-side | Data science, scripting, automation |
| Typical interview focus | Coroutines, sealed classes, interop | Iterators, decorators, GIL behavior |
| Concurrency model | Coroutine-based, structured | Threads, asyncio, GIL-bound |
How to decide which to prepare for:
- Match the language to the job description — Android and backend-on-JVM roles lean Kotlin; data, ML, and DevOps roles lean Python.
- Consider your existing fluency. If you already know Java, Kotlin's syntax and JVM semantics will feel familiar; if you come from a scripting or analytics background, Python's concepts transfer more directly.
- Look at what the interview actually tests. Kotlin rounds often include coroutine sequencing and sealed-class modeling; Python rounds frequently ask about generator behavior, decorator mechanics, and mutable-default-argument traps.
- Check the live question panels below for both languages to see real attempt data and gauge where your gaps are.
Kotlin vs Python, side by side
How Kotlin and Python compare on Tarmac’s interview questions.
| Metric | Kotlin | Python |
|---|---|---|
| Practice questions | 6 | 6 |
| Average score | — | — |
| Hardest question (% who miss it) | — | — |
| Average time per question | — | — |
Practice questions
In Kotlin, a value class (declared with value class on the JVM) must have exactly one read-only property in its primary constructor.#
Options
- True
- False
Show answer
True. A Kotlin value class must declare exactly one read-only (val) property in its primary constructor; the compiler inlines this single underlying value wherever possible and rejects zero- or multi-property definitions at compile time.
The Kotlin language spec requires every value class to wrap exactly one underlying value, declared as a val property in the primary constructor. A value class with zero properties or with more than one property will not compile.
What is printed by the following code? Assume the module is run as __main__ and Node is fully defined before get_type_hints is called.#
from __future__ import annotations
from typing import Optional, get_type_hints
class Node:
def __init__(self, val: int, next: Optional[Node] = None) -> None:
self.val = val
self.next = next
hints = get_type_hints(Node.__init__)
print(hints['next'])Show answer
typing.Optional[__main__.Node]
Python's typing.get_type_hints() resolves forward references and applies __future__ annotations. When from __future__ import annotations is active, ALL annotations are stored as strings (PEP 563 postponed evaluation). get_type_hints() then resolves them at call time using the provided (or inferred) global namespace. Here the forward reference 'Node' is resolved by get_type_hints() to the actual Node class, so hints['next'] returns typing.Optional[Node]. Printing that gives typing.Optional[__main__.Node] (or the equivalent module path). The __annotations__ dict, by contrast, would still hold the raw string.
Which functions does the Kotlin compiler automatically generate for a data class based on the properties declared in its primary constructor?#
Options
- clone(), equals(), hashCode(), and toString()
- equals(), hashCode(), toString(), copy(), and componentN()
- equals(), hashCode(), toString(), and deepCopy()
- compareTo(), clone(), serialize(), and componentN()
Show answer
Kotlin's data class modifier causes the compiler to auto-generate equals(), hashCode(), toString(), copy(), and componentN() functions for the properties declared in the primary constructor. The componentN() functions enable destructuring declarations, and copy() creates a shallow copy with optional property overrides.
For each property in the primary constructor, the Kotlin compiler generates equals()/hashCode() based on those properties, toString() returning the class name and property values, copy() for creating a shallow copy with optional overrides, and componentN() functions to support destructuring declarations.
You have a pytest test that should be skipped when the environment variable CI is not set OR is set to an empty string (i.e., when CI is falsy). Which decorator achieves this correctly?#
Options
- Option A —
os.getenv("CI") is None: skips only whenCIis completely absent, but runs whenCI="" - Option B —
not os.getenv("CI"): skips whenCIis absent or set to an empty string, treating both as 'not usefully set' - Option C —
@pytest.mark.skip: unconditionally skips the test regardless of any environment variable - Option D —
os.getenv("CI") is not None: skips whenCIis set, which is the opposite of the intended behaviour
Show answer
The correct decorator is @pytest.mark.skipif(not os.getenv("CI"), reason="Only run in CI"). pytest.mark.skipif skips the test when its condition is True, and not os.getenv("CI") is True both when CI is unset and when it is set to an empty string. Checking is None would miss the empty-string case, since os.getenv("CI") returns "" rather than None when CI is exported as empty.
pytest.mark.skipif skips the test when its condition evaluates to True. The requirement here is to skip when CI is falsy — meaning absent or set to an empty string (e.g., CI=). not os.getenv("CI") is True in both of those cases, making Option B correct. Option A (is None) only returns True when the variable is entirely absent; if CI is exported as an empty string, os.getenv("CI") returns "" (not None), so is None is False and the test would run despite CI being empty — missing the falsy case. Option D inverts the logic and skips when CI is present. Option C unconditionally skips, ignoring the environment variable entirely.
Place the following events in the order they occur when a Kotlin object declaration is first accessed at runtime.#
Put these in order
- The object's class is loaded and its static initializer is triggered on first access
- The declared superclass constructor (if any) executes
- Property initializers and init blocks execute in textual (top-to-bottom) order
- The fully initialized singleton instance is returned to the caller
Show answer
When a Kotlin object is first accessed, its class loads and the static initializer fires, then the superclass constructor runs, then property initializers and init blocks execute in top-to-bottom textual order, and finally the fully constructed singleton instance is returned to the caller. Kotlin objects are lazily initialized on first use.
Kotlin object declarations are initialized lazily — the singleton is not created until first access (the object's class is loaded and its static initializer is triggered on first access). When initialization begins, the superclass constructor runs first (the declared superclass constructor (if any) executes), mirroring normal class construction. Then property initializers and init blocks run in textual order (property initializers and init blocks execute in textual (top-to-bottom) order) — so x is initialized, then the init block runs, then y. Only after all initialization completes is the fully constructed instance handed back to the caller (the fully initialized singleton instance is returned to the caller).
Given a = [1, 2] and b = [1, 2], what are a == b and a is b?#
Options
TrueandFalseTrueandTrueFalseandFalseFalseandTrue
Show answer
a == b is True and a is b is False. The == operator compares values, and both lists hold the same elements, so it is True. The is operator compares identity — whether two names point at the same object in memory — and because these are two separately-constructed lists, it is False.
== compares values, and the two lists hold the same elements, so it is True. is compares identity — whether both names point at the same object in memory — and these are two separately-constructed lists, so it is False.
What is the exact stdout produced by the following Kotlin program? Trace the side effects carefully, paying attention to how List operations differ from Sequence operations in when and how elements flow through the pipeline.#
fun main() {
val list = listOf(1, 2, 3, 4, 5)
// --- List (eager) pipeline ---
list.filter {
print("F$it ")
it % 2 == 0
}.map {
print("M$it ")
it * 10
}
println()
println("---")
// --- Sequence (lazy) pipeline ---
list.asSequence()
.filter {
print("F$it ")
it % 2 == 0
}
.map {
print("M$it ")
it * 10
}
.toList()
}Show answer
F1 F2 F3 F4 F5 M2 M4
---
F1 F2 M2 F3 F4 M4 F5
Kotlin List chains are eager: each intermediate operation completes over the entire collection before the next one starts. So filter walks all five elements first — printing F1 F2 F3 F4 F5 — and only then does map run on the survivors [2, 4], printing M2 M4 . A Sequence, by contrast, is lazy: elements are pulled one at a time through the entire pipeline when a terminal operation (toList()) is invoked. Element 1 enters the filter (F1 ), is rejected, and never reaches map. Element 2 passes the filter (F2 ) and immediately flows into map (M2 ). Element 3 is filtered out (F3 ), element 4 passes both stages (F4 M4 ), and element 5 is filtered out (F5 ). This interleaved, element-by-element order — F1 F2 M2 F3 F4 M4 F5 — is the hallmark of lazy sequence evaluation and the key performance reason sequences avoid intermediate collections.
What does the second call return?#
Options
[1, 2][2][1]- It raises a
TypeError
Show answer
The second call returns [1, 2]. A default argument is evaluated only once, when the function is defined, so every call that omits acc shares the same list. The first call appends 1 and the second appends 2 to that same list. The fix is to default acc=None and create a fresh list inside the function.
A default argument is evaluated once, when the function is defined, so all calls that omit acc share the same list. The first call appends 1, the second appends 2 to that same list, giving [1, 2]. The fix is acc=None plus if acc is None: acc = [].
What does the following Kotlin code print?#
fun main() {
val mutable: MutableList<Int> = mutableListOf(1, 2, 3)
val readOnly: List<Int> = mutable
mutable.add(4)
println(readOnly.size)
}
Show answer
4
mutable and readOnly both reference the same underlying ArrayList object. Assigning a MutableList to a List-typed variable does not copy the data or create a defensive wrapper — List is simply a narrower read-only interface view onto the same object. When mutable.add(4) is called, the shared backing list grows to four elements. Therefore readOnly.size returns 4, demonstrating that Kotlin's List interface is read-only, not truly immutable: mutations through any reference to the same mutable backing object are visible through every other reference.
In CPython, what is the practical effect of the Global Interpreter Lock (GIL) on a pure-Python, CPU-bound workload?#
Options
- Only one thread executes Python bytecode at a time, so threads do not speed up CPU-bound work — use multiprocessing for true parallelism
- Threads run Python bytecode on all cores in parallel, so the GIL has no effect on CPU-bound speed
- The GIL prevents I/O-bound threads from ever overlapping their waits
- The GIL makes every operation on built-in types fully thread-safe regardless of how it is used
Show answer
Only one thread executes Python bytecode at a time, so threads do not speed up CPU-bound work — the work is serialized. The GIL is released during blocking I/O, so threads still help I/O-bound work, but for true CPU parallelism you reach for multiprocessing, which runs separate interpreters in separate processes.
The GIL lets only one thread run Python bytecode at any instant, so threading gives no speedup on CPU-bound Python code — the work is serialized. Threads still help for I/O-bound work because the GIL is released during blocking I/O, but for CPU parallelism you reach for multiprocessing (separate interpreters) instead.
The following Kotlin code fails to compile. What is the bug?#
sealed class Operation
data class Add(val a: Int, val b: Int) : Operation()
data class Subtract(val a: Int, val b: Int) : Operation()
data class Multiply(val a: Int, val b: Int) : Operation()
fun execute(op: Operation): Int = when (op) {
is Add -> op.a + op.b
is Subtract -> op.a - op.b
}Options
- The
whenexpression does not cover theMultiplysubclass, so it is non-exhaustive and the code will not compile. sealed classcannot havedata classsubclasses.executeshould returnInt?because no branch matches when the operation isMultiply.- Subclasses of a
sealed classmust be defined inside the class body.
Show answer
The when expression does not cover the Multiply subclass, so it is non-exhaustive and the code will not compile.
The when expression is used as the return value of execute, making it an expression form that Kotlin requires to be exhaustive. Operation is a sealed class with three direct subclasses: Add, Subtract, and Multiply. The when only handles Add and Subtract, omitting Multiply. Because sealed classes enable exhaustiveness checking, the compiler rejects the non-exhaustive when. Adding an is Multiply -> op.a * op.b branch (or an else branch) would fix the compile error.
What is the key difference between [x*x for x in range(10**9)] and (x*x for x in range(10**9))?#
Options
- The generator expression produces values lazily one at a time, using O(1) memory; the list comprehension builds all 10**9 values up front
- They are identical — both build a full list, just with different syntax
- The generator is faster because it stores results in a tuple instead of a list
- The list comprehension is lazy and the generator is eager
Show answer
The generator expression produces values lazily, one at a time, using O(1) memory, whereas the list comprehension builds all 10**9 values up front. Square brackets eagerly materialize every element into a list, while parentheses create a generator that computes each value on demand and holds only the current state. The trade-off is that a generator is single-pass with no len() or indexing.
Square brackets eagerly materialize every element into a list, so the comprehension here would try to allocate a billion integers. Parentheses create a generator that computes each value on demand and yields it once, holding only the current state — constant memory. The trade-off is that a generator is single-pass and has no len() or indexing.