1. Why you need vararg
Sometimes in programming you run into a very everyday situation: you want to write a function that performs the same action for a set of values, but you don’t know in advance how many values there will be. Today it’s 2, tomorrow 5, the day after tomorrow 0 (yes, that happens too, and it’s not always a bug).
You can, of course, force the calling code to manually assemble an array, you can pass “empty values” like 0 or "" (and then suffer), you can write different functions “for 2 arguments”, “for 3 arguments”… but it quickly turns into a circus.
For these cases Kotlin has a parameter with a variable number of arguments—vararg.
What vararg is
When you see the word vararg in a function signature, it means: “you can pass 0 or more arguments of this type into this function.” So the range of the number of arguments looks like 0..N.
Formally, this is part of the Kotlin language: one parameter in the signature is marked with the vararg modifier, and Kotlin allows calling the function by listing any number of values. Inside the function, this parameter behaves like an array—we’ll get to that a bit later.
Let’s start with the most “classic” example—sum.
2. Syntax and basic vararg calls
Minimal example: sum as many numbers as you want
Now there will be code that looks too simple—and that’s fine. Our goal is for you to see what vararg looks like and what the call looks like.
fun sumAll(vararg xs: Int): Int {
var sum = 0
for (x in xs) sum += x
return sum
}
fun main() {
println(sumAll(1, 2, 3)) // 6
println(sumAll(10, 20)) // 30
}
Notice two things. First, vararg xs: Int sits where a normal parameter would—this is still a “function parameter”, just a special one. Second, the call sumAll(1, 2, 3) looks like a regular function call where you listed arguments separated by commas.
Call with no arguments: vararg allows f()
With vararg there’s almost always a small “moment of truth”: you suddenly realize that the function is legally callable with no arguments at all. This is not a compilation error; Kotlin won’t complain. And this is where you, as the function author, must think: “What should my function do in that case?”
Let’s continue the sum example—for the sum of an “empty set” it makes sense to return 0.
fun sumAll(vararg xs: Int): Int {
var sum = 0
for (x in xs) sum += x
return sum
}
fun main() {
println(sumAll()) // 0
println(sumAll(5)) // 5
}
In this example, sumAll() returns 0 not because Kotlin “decided so”, but because we wrote code that, for an empty set, simply won’t enter the loop and will return the initial value sum.
What vararg looks like in a signature and how to read it
At this stage it’s important to learn to “read” the signature at a glance. When you see:
fun printLines(vararg lines: String) { ... }
it reads roughly like: “printLines accepts any number of strings, and inside we’ll process them one by one.”
In Kotlin, vararg is usually placed closer to the end of the parameter list (often the very last one), because calls look more natural that way.
Let’s make a “practical” printing function:
fun printLines(vararg lines: String) {
for (line in lines) {
println(line)
}
}
fun main() {
printLines("one", "two", "three")
// one
// two
// three
}
And here vararg really starts to feel like convenience: instead of manually creating an array of strings “just to print once”, you simply list the values.
3. How vararg works inside a function
What happens “under the hood”: vararg becomes an array
Now comes an important idea that helps a lot to avoid confusion. When you call sumAll(1, 2, 3), Kotlin, inside the compiler, does roughly this: it “packs” these numbers into an array and passes it into the function.
In practice, this means: inside the function you have access to familiar array things—the for (x in xs) loop, the size xs.size, indexing xs[i].
To visualize it, you can imagine the process like this:
flowchart TD
A["Call: sumAll(1, 2, 3)"] --> B["Compiler packed into a collection"]
B --> C["Inside the function, xs behaves like an array"]
C --> D["The for (x in xs) loop computes the sum"]
You don’t need to memorize how exactly it’s stored in memory—it’s enough to remember the practical rule: inside the function vararg can be iterated like an array.
vararg inside a function: size, indices, and printing
Once you’ve gotten a bit comfortable with for (x in xs), almost immediately you want to do something smarter: for example, print elements with their numbers or figure out how many arguments came in. And that’s a normal desire: you start thinking like a developer, not like someone who just “copies an example”.
Let’s make a function that prints numbers and their count:
fun printNumbers(vararg xs: Int) {
println("Count = ${xs.size}")
for (i in xs.indices) {
println("xs[$i] = ${xs[i]}")
}
}
fun main() {
printNumbers(7, 8, 9)
// Count = 3
// xs[0] = 7
// xs[1] = 8
// xs[2] = 9
}
Here you see a combination from previous topics: indices on arrays and indexing xs[i]. This is exactly the same style as in the arrays topic; now the array just “arrives” into the function via vararg.
Limitation: there can be only one vararg in a function
Kotlin has an important rule: only one function parameter can be marked as vararg.
This is not the language “being picky”, but a way to avoid ambiguities: if you could write fun f(vararg a: Int, vararg b: Int), then on the call f(1, 2, 3) it would be unclear where a ends and where b begins.
Here is an example that won’t compile:
fun broken(vararg a: Int, vararg b: Int) { // not allowed
println(a.size + b.size)
}
The compiler won’t let you build this—and thereby it saves you from code that can’t be called in a reasonable way.
4. Practice: vararg in “Guess the Number”
Why vararg is useful in a project
vararg is not a “fun language feature”, but a tool that makes code more convenient and cleaner—especially when you print a lot of messages or collect data in one place.
Imagine that in your mini-project “Guess the Number” you often do chunks like this:
println("Welcome!")
println("I picked a number.")
println("Try to guess it.")
It works, but it looks noisy: a lot of print lines, and the game logic drowns in “chatter”.
Let’s make a small utility: “print several lines”. And here vararg is perfect: the number of lines is different each time.
fun say(vararg lines: String) {
for (line in lines) {
println(line)
}
}
Now in main or in your game loop you can write more compactly and readably.
Example: game greeting via say(vararg ...)
Important: the code examples will be short, but logically connected. Imagine these are fragments from a single file GuessNumber.kt that you already made earlier.
fun say(vararg lines: String) {
for (line in lines) println(line)
}
fun main() {
say(
"Welcome to the game!",
"I picked a number.",
"Try to guess it :)"
)
}
Notice: syntactically, this is a regular function call. There are just many arguments. Kotlin doesn’t require any brackets, arrays, or extra steps. You list the strings, and the function receives them as a “bundle”.
Example: hint messages as vararg
In “Guess the Number” there is often branching: we print different hints depending on the situation. And sometimes you want to print one line, sometimes two. If printing is smeared across if/else, the code quickly starts to look like holiday string lights: it shines, but it’s unclear where the wire is.
Let’s make a function that prints hints:
fun hint(vararg lines: String) {
for (line in lines) println(line)
}
fun main() {
val secret = 42
val guess = 50
if (guess > secret) {
hint("Too high!", "Try smaller.")
// Too high!
// Try smaller.
} else if (guess < secret) {
hint("Too low!")
// Too low!
} else {
hint("You guessed it!")
// You guessed it!
}
}
Here you essentially made a “micro-API” for messages. And it’s a nice habit: you separate “what we do” (game logic) from “how we talk” (messages to the user).
Example: debug(vararg ...) for debug printing
When you’re learning, you often insert println() just to understand “what’s going on”. That’s fine. Moreover, it’s one of the most honest debugging methods early on (before you’re comfortable with a debugger).
But if there are too many println()s, you get noise again. You can make a tiny “logger” on a budget: the function takes a tag (a string) and a set of messages.
Important: today we’re making the simplest possible variant, where vararg is last so the call feels natural.
fun debug(tag: String, vararg messages: String) {
for (m in messages) {
println("[$tag] $m")
}
}
fun main() {
debug("GAME", "Start", "Secret generated", "Waiting for input")
// [GAME] Start
// [GAME] Secret generated
// [GAME] Waiting for input
}
This gives you convenient output, and vararg lets you pass as many messages as you want in a single call.
5. vararg or an array: what to choose
Before vararg, you already had a way to pass “many values”: make a parameter of an array type, for example IntArray. That approach works. But vararg often makes the call simpler and nicer to read.
Here’s a compact comparison:
| Approach | What the call looks like | What’s inconvenient | When it’s appropriate |
|---|---|---|---|
|
|
you need to create the array manually | when the data already lives in an array anyway |
|
|
you need to think through the case |
when you want a “natural” call by listing values |
We’ll talk separately about the situation “the data is already in an array, but the function is vararg” in the lecture on the spread operator, because there are important syntax nuances there. Today we won’t go there: what matters is to understand the idea of vararg and get used to calling it.
6. Typical mistakes when working with vararg
Mistake #1: “I thought vararg was a magical collection, but it’s like an array.”
At the start it’s easy to expect vararg to have some “super methods”. But in practice it’s simpler and more correct to think like this: inside the function vararg behaves like an array, and the basic operations are for, size, and indexing.
Mistake #2: forgetting that a call with no arguments is possible, and accidentally crashing on xs[0].
As soon as you write vararg, you automatically allow calling f(). If inside you immediately do xs[0], then with an empty set you’ll get a runtime error. Even if “in your scenario it never happens”, Kotlin still allows such a call, and you must either handle it or explicitly forbid it.
Mistake #3: trying to declare two vararg parameters in one function.
This is a classic: you want “a list of numbers and a list of strings”, but Kotlin won’t allow it—there can be only one vararg. This is a language limitation, and it’s designed so calls remain unambiguous.
Mistake #4: using vararg “just in case”, even though there are always exactly two arguments.
vararg is a powerful tool, but if by meaning your function always has two values (for example, width and height), then vararg makes readability worse: the call starts to look like there can be any number of values, even though there can’t. At the API level this is misleading, so it’s better to keep normal parameters when the count is fixed.
Mistake #5: turning vararg into a way to “hide bad design”.
Sometimes you see an approach like “let’s just pass everything via vararg and figure it out inside”. This is almost always bad: the function becomes unpredictable, and errors show up late. vararg is good when the set is homogeneous and logical (messages, numbers, strings with a single meaning), not when it’s a bag of random stuff.
Mistake #6: doing too much printing via say(vararg lines) and losing the program structure.
say(vararg lines: String) is useful, but if you start printing “everything in the world” through it, it’s easy to lose track of where your logic is and where your UI is. A good guideline: vararg functions usually do one small job (printing, counting, simple set processing) and don’t replace your program’s architecture.