A deep, learnpython.org-style walkthrough of nearly every major Kotlin language feature.
35+ lessons from “Hello World” to generics, sealed types, delegation, sequences, and coroutines.
Part I — Basics
1 Getting started
Hello, Kotlin
Every program starts with main — the entry point the runtime calls.
Hello.kt
funmain() {
println("Hello, Kotlin!")
}
Hello, Kotlin!
println prints a line and adds a newline. print does not.
var — mutable reference. Use only when reassignment is required.
Kotlin
val language = "Kotlin"var year = 2011
year = 2026println("$language — $year")
Kotlin — 2026
Type can be explicit: val name: String = "Ada". Usually the compiler infers it.
Rule: Start with val. Switch to var only when you must reassign.
3 Basic types
Numbers, Booleans, Characters
Int (32-bit), Long (64-bit, suffix L)
Double, Float (suffix f)
Boolean — true / false
Char — single character in single quotes
String — text in double quotes
Byte, Short, UInt, ULong also exist
Kotlin
val a = 10val b = 3.14val big = 1_000_000_000Lval ok = trueval ch = 'K'println(a + 5)
println(b * 2)
15
6.28
Underscores improve readability: 1_000_000.
Type conversion
No implicit widening. Convert explicitly:
Kotlin
val i: Int = 42val d: Double = i.toDouble()
val s: String = i.toString()
val back = "42".toInt()
val safe = "x".toIntOrNull() // null
4 Strings
Templates, operations, multi-line
String templates
Kotlin
val name = "Kotlin"val v = 2.0println("Language: $name")
println("Next: ${v + 0.21}")
Language: Kotlin
Next: 2.21
Common operations
Kotlin
val s = " Hello World "println(s.trim())
println(s.uppercase())
println(s.contains("World"))
println(s.replace("World", "Kotlin"))
println(s.substring(2, 7))
println(s.split(" "))
println(s[2]) // char at index
Raw / multi-line strings
Kotlin
val text = """
|Line one
|Line two
""".trimMargin()
println(text)
StringBuilder (efficient mutation)
Kotlin
val sb = StringBuilder()
sb.append("Hello").append(" ").append("Kotlin")
println(sb.toString())
val age = 20val status = if (age >= 18) "adult"else"minor"println(status)
val max = if (a > b) a else b
adult
No ternary ? : — use if expressions. Chain with else if.
7 Loops & ranges
for, while, ranges
Kotlin
for (i in1..5) print("$i ")
println()
for (i in5downTo1) print("$i ")
println()
for (i in0..10step2) print("$i ")
println()
var n = 3while (n > 0) { println(n); n-- }
// break / continue work as expected// labels: outer@ for (...) { break@outer }
1 2 3 4 5
5 4 3 2 1
0 2 4 6 8 10
3
2
1
until is exclusive upper bound: 0 until 5 → 0..4.
Part II — Functions & Null Safety
8 Functions
Declaration, defaults, named args, varargs
Kotlin
fungreet(name: String): String = "Hello, $name!"funconnect(host: String = "localhost", port: Int = 8080) {
println("$host:$port")
}
connect()
connect(port = 3000)
connect("api.example.com", 443)
// varargfunsum(vararg nums: Int) = nums.sum()
println(sum(1, 2, 3, 4))
// Unit is the return type of functions that return nothing (like void)funlog(msg: String) { println(msg) }
tailrec funfactorial(n: Int, acc: Int = 1): Int =
if (n <= 1) acc elsefactorial(n - 1, n * acc)
println(factorial(5)) // 120
9 Null safety
The type system that kills NPEs
Types are non-null by default. Add ? to allow null.
Kotlin
var a: String = "Hi"// never nullvar b: String? = null// can be null// Safe callprintln(b?.length)
// Elvis — default when nullval len = b?.length ?: 0// Not-null assertion (throws if null — use rarely)// val forced = b!!// Safe castval s = a as?String// null if cast fails// let with nullables
b?.let { println("Got: $it") }
lateinit & lazy
Kotlin
lateinit var config: String// must assign before read (var only)val expensive bylazy {
println("Computing…")
42
}
println(expensive) // computes onceprintln(expensive) // cached
Part III — Collections
10 Lists
Ordered collections
Kotlin
val fruits = listOf("apple", "banana", "cherry")
println(fruits[0])
println(fruits.size)
println(fruits.contains("banana"))
val numbers = listOf(1, 2, 3, 4, 5, 6)
println(numbers.filter { it % 2 == 0 })
println(numbers.map { it * 2 })
println(numbers.sum())
println(numbers.reduce { acc, n -> acc + n })
println(numbers.groupBy { it % 2 == 0 })
println(numbers.take(3))
println(numbers.drop(2))
println(numbers.sortedDescending())
println(numbers.zip(fruits))
val mutable = mutableListOf(1, 2)
mutable.add(3)
mutable[0] = 10
Resolved statically. They do not override members — a real member always wins.
23 Lambdas & higher-order functions
Functions as values
Kotlin
val add: (Int, Int) -> Int = { a, b -> a + b }
println(add(2, 3))
funoperate(x: Int, y: Int, op: (Int, Int) -> Int) = op(x, y)
println(operate(4, 5) { a, b -> a * b })
// Trailing lambda + implicit itlistOf(1, 2, 3).forEach { println(it) }
// Function referencesfuntriple(n: Int) = n * 3println(listOf(1, 2).map(::triple))
// Returning from lambda — use labelslistOf(1, 2, 3).forEach {
if (it == 2) return@forEachprintln(it)
}
24 Scope functions
let, run, with, apply, also
Function
Object ref
Returns
Typical use
let
it
lambda result
null checks, transform
run
this
lambda result
compute on object
with
this
lambda result
non-extension variant of run
apply
this
object
configure / builder
also
it
object
side effects / logging
Kotlin
val name: String? = "Ada"
name?.let { println("Hi $it") }
val list = mutableListOf<Int>().apply {
add(1); add(2)
}
val s = "kotlin".run { uppercase().reversed() }
println(s) // NILTOK
Part VI — Advanced Language Features
25 Generics
Type parameters, variance, reified
Kotlin
classBox<T>(val value: T)
fun <T> singletonList(item: T) = listOf(item)
// Upper boundsfun <T : Comparable<T>> max(a: T, b: T) = if (a > b) a else b
// Variance// out T = covariant (producer) — List<out T>// in T = contravariant (consumer)interfaceProducer<out T> { funproduce(): T }
interfaceConsumer<in T> { funconsume(item: T) }
// Reified type parameters (only with inline)inline fun <reified T> isType(value: Any) = value is T
println(isType<String>("hi")) // true// Star projectionval anyList: List<*> = listOf(1, "a")
Use out when the type only appears in output positions, in for input positions (PECS: Producer Extends, Consumer Super).
26 Delegation
by — class & property delegation
Kotlin
interfaceBase { funprint() }
classBaseImpl(val x: Int) : Base {
override funprint() = println(x)
}
// Class delegationclassDerived(b: Base) : Baseby b
Derived(BaseImpl(10)).print()
// Property delegationclassExample {
var p: StringbyDelegates.observable("init") { _, old, new ->
println("$old → $new")
}
val lazyVal: Stringbylazy { "computed" }
var mapProp: StringbymutableMapOf() // map delegation
}
10
Standard delegates: lazy, observable, vetoable, map delegation.
27 Inline functions
Avoid lambda allocations + reified types
Kotlin
inline fun <T> measure(block: () -> T): T {
val start = System.nanoTime()
val result = block()
println("Took ${System.nanoTime() - start} ns")
return result
}
// noinline — keep a lambda as objectinline funfoo(noinline cb: () -> Unit) { /* store cb */ }
// crossinline — forbid non-local returns in lambdainline funrunSafe(crossinline body: () -> Unit) {
try { body() } catch (e: Exception) {}
}
// Reified (only possible with inline)inline fun <reified T> List<*>.filterIsInstance() =
filter { it is T }.map { it as T }
The compiler inlines the function body at call sites, eliminating the cost of lambda objects.
28 Operator overloading
Make your types work with +, [], in, …
Kotlin
data classPoint(val x: Int, val y: Int) {
operator funplus(o: Point) = Point(x + o.x, y + o.y)
operator fununaryMinus() = Point(-x, -y)
operator funget(index: Int) = if (index == 0) x else y
operator funinvoke() = println("($x, $y)")
}
val p = Point(1, 2) + Point(3, 4)
println(p) // Point(x=4, y=6)println(-p)
println(p[0])
p() // invoke
Point(x=4, y=6)
Point(x=-4, y=-6)
4
(4, 6)
Other operators: minus, times, div, rem, contains (in), compareTo, rangeTo, inc/dec, etc.
29 Exceptions
try as expression, Nothing, checked exceptions
Kotlin
funparse(s: String): Int =
try {
s.toInt()
} catch (e: NumberFormatException) {
0
} finally {
println("done")
}
println(parse("42"))
println(parse("x"))
// throw returns Nothing (type that never completes)funfail(msg: String): Nothing {
throwIllegalStateException(msg)
}
val s: String = name ?: fail("name required")
// Kotlin does not have checked exceptions// @Throws is only for Java interop
done
42
done
0
30 Sequences
Lazy collection processing
Iterable operations are eager (each step creates a full collection).
Sequence is lazy — intermediate results are not materialized until a terminal operation.
Kotlin
val result = (1..1_000_000)
.asSequence()
.map { it * it }
.filter { it % 2 == 0 }
.take(5)
.toList()
println(result)
// Generate infinite sequencesval fib = generateSequence(1 to 1) { (a, b) -> b to a + b }
.map { it.first }
.take(10)
.toList()
println(fib)
Aliases are erased at compile time — they do not create new types. Useful for readability and long function types.
33 Value classes (inline classes)
Zero-cost wrappers
Kotlin
@JvmInlinevalue classPassword(private val s: String) {
init { require(s.length >= 8) }
override funtoString() = "*".repeat(s.length)
}
val p = Password("secret123")
println(p) // *********// At runtime this is often just a String — no allocation
Value classes must have a single property in the primary constructor. They provide type safety without runtime overhead in many cases.
34 Annotations
Metadata for the compiler and tools
Kotlin
@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
annotation classAuth(val role: String = "user")
@Auth("admin")
classAdminPanel// Common standard annotations@JvmStatic// expose as static to Java@JvmOverloads// generate overloads for defaults@JvmField// expose as field, no getter@Throws(IOException::class)
@Deprecated("Use newApi()", ReplaceWith("newApi()"))
@Suppress("UNCHECKED_CAST")
@OptIn(ExperimentalStdlibApi::class)
Annotations can have constructors and be applied to classes, functions, properties, parameters, expressions, and more.
These 36 lessons span the vast majority of everyday and advanced Kotlin.
What remains is mostly the standard library surface area, multiplatform specifics, and ecosystem libraries.