Default and Named Arguments

In many languages, if you want a function parameter to be optional, you either write several overloaded versions of the function or force every caller to pass every argument, even when a sensible default would do. Kotlin solves this directly with default arguments (a parameter can declare its own fallback value) and named arguments (a caller can specify which parameter a value belongs to, by name, instead of relying purely on position). Together these two features remove most of the reason Java-style APIs need telescoping constructors or dozens of overloads, and they make call sites read like documentation.

Overview: How Default and Named Arguments Work

A default argument is a value you attach to a parameter in the function’s declaration. If the caller omits that argument, the compiler substitutes the default at the call site — this happens entirely at compile time, not through some runtime lookup. Any parameter can have a default, and a function can mix required parameters (no default) with optional ones (default supplied), in any combination, as long as the parameter list is unambiguous to the compiler.

Named arguments let a caller write parameterName = value instead of relying on position. This is independent of defaults — you can use named arguments on a function that has no default values at all, purely to make a call more readable — but the two features are usually discussed together because named arguments are what make defaults practical. Without named arguments, if you wanted to override only the third of five optional parameters, you would have to pass the first two explicitly just to reach it positionally. With named arguments, you skip straight to the one you care about.

Under the hood, the Kotlin compiler resolves every call at compile time: it matches each argument (positional or named) to exactly one parameter, fills in the declared default expression for any parameter left unmatched, and verifies the result satisfies every parameter’s type. This is why a program with an invalid combination of named/positional arguments, or a missing required parameter, fails to compile rather than crashing at runtime — the same category of safety Kotlin applies to its null-safety checks.

One subtlety worth internalizing early: a default value expression can reference any parameter declared before it in the list (they are evaluated left to right at the call site when needed), but it cannot reference a parameter declared after it, and it cannot reference this in most contexts before the object exists. Also, when a function is open and overridden in a subclass, the override is not allowed to redeclare a default value — more on why in Common Mistakes below.

Syntax

The general form attaches = defaultValue directly after a parameter’s type in the declaration:

fun functionName(param1: Type1 = default1, param2: Type2 = default2): ReturnType {
    // function body
}

// call using named arguments, in any order:
functionName(param2 = someValue, param1 = otherValue)
Part Meaning
param: Type = default Declares a parameter with a fallback value used whenever the caller omits it.
functionName(x = value) A named argument — explicitly binds value to parameter x regardless of its position.
Positional argument An argument matched to a parameter purely by its position in the call.
Mixing rule You may mix positional and named arguments in one call, but every positional argument must appear before the first named one.

Examples

Example 1: A single default parameter

fun greet(name: String, greeting: String = "Hello"): String {
    return "$greeting, $name!"
}

fun main() {
    println(greet("Alice"))
    println(greet("Bob", "Hi"))
}

Output:

Hello, Alice!
Hi, Bob!

The first call omits greeting entirely, so the compiler substitutes the default string "Hello". The second call supplies both arguments positionally, overriding the default. Note that name has no default, so it must always be supplied — Kotlin does not require optional parameters to come last, but it is the conventional and most readable ordering.

Example 2: Named arguments to skip and reorder parameters

fun createUser(name: String, age: Int = 18, isAdmin: Boolean = false): String {
    return "User(name=$name, age=$age, isAdmin=$isAdmin)"
}

fun main() {
    println(createUser("Charlie"))
    println(createUser("Dana", isAdmin = true))
    println(createUser(age = 30, name = "Eve"))
}

Output:

User(name=Charlie, age=18, isAdmin=false)
User(name=Dana, age=18, isAdmin=true)
User(name=Eve, age=30, isAdmin=false)

The second call needs to override only isAdmin, so it names that one argument and lets age fall back to its default — no need to pass 18 explicitly just to reach the third parameter. The third call names both arguments and swaps their order entirely; because both are named, the compiler doesn’t care what order you write them in.

Example 3: Defaults that depend on another parameter’s value

fun formatPrice(amount: Double, currency: String = "USD", showSymbol: Boolean = true): String {
    val symbol = when (currency) {
        "USD" -> "$"
        "EUR" -> "€"
        "GBP" -> "£"
        else -> ""
    }
    return if (showSymbol) "$symbol$amount $currency" else "$amount $currency"
}

fun main() {
    println(formatPrice(19.99))
    println(formatPrice(19.99, "EUR"))
    println(formatPrice(19.99, showSymbol = false))
}

Output:

$19.99 USD
€19.99 EUR
19.99 USD

This is a more realistic case: most callers only care about the amount, so currency and showSymbol both default. The third call demonstrates naming an argument purely for clarity even though it’s the third parameter and there’s nothing to skip — a reader instantly knows showSymbol = false means without a symbol, whereas a bare false at a call site would require checking the function signature.

Example 4: Default arguments in a constructor, exposed to Java with @JvmOverloads

class Invoice @JvmOverloads constructor(
    val amount: Double,
    val tax: Double = 0.0,
    val discount: Double = 0.0
) {
    fun total(): Double = amount + tax - discount
}

fun main() {
    val invoice = Invoice(100.0, tax = 8.0)
    println(invoice.total())
}

Output:

108.0

Default arguments apply to constructors exactly the same way they apply to functions. The @JvmOverloads annotation is worth knowing about even though it’s not required for this Kotlin-only example: default parameter values are a purely Kotlin-compiler feature, so a plain Kotlin function with defaults compiles down to a single JVM method that always expects every argument. Java code calling it would have to pass all three values explicitly. Annotating the constructor (or function) with @JvmOverloads tells the compiler to also generate the overloaded JVM methods Java callers expect, one for each suffix of optional parameters dropped.

How It Works Step by Step

Walking through the call createUser(age = 30, name = "Eve") from Example 2:

  • The compiler reads the argument list and finds two named arguments: age and name.
  • It matches age = 30 to the age: Int parameter and name = "Eve" to the name: String parameter — position in the call is irrelevant once arguments are named.
  • It notices isAdmin was not supplied by the caller, so it looks up the default expression declared for isAdmin, which is the literal false.
  • All three parameters now have concrete values (name="Eve", age=30, isAdmin=false), so the compiler emits a call as if you had written createUser("Eve", 30, false) — there’s no runtime overhead or reflection involved; this resolution is entirely static.
  • The function body runs normally and returns the formatted string, which println writes to standard output.

Common Mistakes

Mistake 1: Placing a positional argument after a named one

Once you name an argument in a call, every argument after it must also be named — you cannot go back to positional style.

fun power(base: Int, exponent: Int = 2, mod: Int = 0): Int {
    var result = 1
    repeat(exponent) { result *= base }
    return if (mod != 0) result % mod else result
}

fun main() {
    println(power(exponent = 3, 5)) // does not compile: positional argument after a named one
}

The compiler cannot tell whether the trailing 5 is meant for base or something else, so it rejects the whole call. Fix it by naming every argument from that point on, or simply keeping the required argument first:

fun power(base: Int, exponent: Int = 2, mod: Int = 0): Int {
    var result = 1
    repeat(exponent) { result *= base }
    return if (mod != 0) result % mod else result
}

fun main() {
    println(power(5, exponent = 3))
}

Output:

125

Mistake 2: Assuming an override can supply its own default value

Kotlin does not allow a function that overrides an open member to declare new default values for its parameters — the default belongs to the base declaration only.

open class Greeter {
    open fun greet(name: String, punctuation: String = "!") {
        println("Base greet: Hello, $name$punctuation")
    }
}

class ExcitedGreeter : Greeter() {
    override fun greet(name: String, punctuation: String = "?!") {
        println("Excited greet: Hello, $name$punctuation")
    }
}

This fails to compile because an overriding function is not permitted to specify a default value for its parameters. The rule exists because default values are resolved statically, based on the compile-time type of the variable you call through — letting each override redefine its own default would make the resolved value depend on which class wrote the override, not on the object’s actual runtime type, which would be surprising and inconsistent. The fix is to omit the default entirely in the override; it still inherits the base class’s default:

open class Greeter {
    open fun greet(name: String, punctuation: String = "!") {
        println("Base greet: Hello, $name$punctuation")
    }
}

class ExcitedGreeter : Greeter() {
    override fun greet(name: String, punctuation: String) {
        println("Excited greet: Hello, $name$punctuation")
    }
}

fun main() {
    val g: Greeter = ExcitedGreeter()
    g.greet("Sam")
}

Output:

Excited greet: Hello, Sam!

Notice which default was used: because g is declared with the static type Greeter, the compiler fills in Greeter‘s default ("!") for the omitted argument, even though the object’s actual runtime type is ExcitedGreeter and it is that subclass’s overridden body that actually executes and prints the line. Default resolution is static; method dispatch is dynamic — keeping these separate in your head avoids surprises in class hierarchies.

Best Practices

  • Prefer default arguments over overloaded functions when the only difference between overloads is that some parameters take a common fallback value — it’s less code to maintain and keeps one source of truth for behavior.
  • Put required parameters (no default) first, and optional ones (with defaults) after, even though Kotlin doesn’t strictly require this ordering — it keeps positional calls readable and predictable for callers who don’t use named arguments.
  • Use named arguments at any call site with two or more Boolean or same-typed numeric arguments, so readers don’t have to check the function signature to know what true or 42 means.
  • Use named arguments when calling a function with many optional parameters, even if you’re only overriding one in the middle — it avoids passing earlier defaults explicitly just to reach it.
  • Annotate a Kotlin function or constructor with @JvmOverloads if it has default parameters and will be called from Java, so Java callers get real overloaded methods instead of being forced to pass every argument.
  • Don’t try to give an override its own default value; if different subclasses genuinely need different defaults, that’s a sign the default belongs in the caller’s logic, not the function signature.

Practice Exercises

  • Write a function buildUrl(host: String, path: String = "/", port: Int = 443, secure: Boolean = true) that returns a URL string such as https://example.com:443/. Call it once with only host supplied, and once naming secure = false and port = 8080 while skipping path.
  • Write a function repeatString(text: String, times: Int = 2, separator: String = ", ") that joins text repeated times times with separator between copies. Predict the output before running it for repeatString("go", times = 3).
  • Take the Greeter/ExcitedGreeter example from Common Mistakes and add a third subclass FormalGreeter that also overrides greet. Call all three greeters through a List<Greeter> without ever supplying punctuation, and confirm every call still prints with the base class’s "!" default regardless of which override runs.

Summary

  • A default argument is declared with param: Type = value and is substituted by the compiler, at compile time, whenever a caller omits that argument.
  • Named arguments let a caller bind a value to a parameter by name (param = value) instead of by position, in any order.
  • Positional and named arguments can be mixed in one call, but every positional argument must come before the first named one.
  • Default value expressions may reference earlier parameters in the same list, but not later ones.
  • An overriding function cannot declare its own default values; the default always comes from the base declaration and is resolved using the static type of the call, while the overridden method body still runs via normal dynamic dispatch.
  • Use @JvmOverloads to expose Kotlin default arguments as real overloaded methods to Java callers.
  • Default and named arguments together are Kotlin’s idiomatic replacement for Java’s overload-heavy or builder-heavy APIs.