Kotlin Scripts vs Programs

Every Kotlin file is built by the same compiler, but Kotlin gives you two very different ways to turn source code into something that runs: a program, where execution begins at a designated fun main() entry point, and a script — a .kts file whose top-level statements execute directly, top to bottom, with no entry point at all. Knowing which one you are writing matters, because they compile differently, run differently, and fit different jobs: programs for anything you ship and run repeatedly, scripts for quick one-off tasks, automation, and build configuration.

Overview: How Scripts and Programs Differ

A regular Kotlin source file (.kt) only allows declarations at its top level — functions, properties, classes, objects, interfaces. It does not allow bare executable statements like a function call sitting on its own outside any function body. That’s why every runnable Kotlin program needs a top-level fun main(): the JVM (or Kotlin/Native, or Kotlin/JS) launcher looks for that function and calls it, and everything the program does has to happen inside it (directly or through functions it calls).

A Kotlin script file (.kts) relaxes that rule. Inside a .kts file, top-level statements are allowed and are executed in the order they appear — no fun main() needed, no explicit call required. Under the hood, the compiler turns a .kt file’s top-level declarations into a synthetic class named after the file (a file called Hello.kt becomes a class called HelloKt behind the scenes), and fun main() becomes the static entry-point method the runtime looks for. A .kts file, by contrast, is compiled into a class whose constructor body holds all of the file’s top-level statements in order. Running a script means instantiating that class, and instantiating a class runs its constructor immediately — which is exactly why a script’s code executes as soon as it’s compiled, without you ever calling anything.

This has real consequences. A script is compiled and discarded on every run (via kotlinc -script or the kotlin launcher pointed at a .kts file); there is no separately reusable class file sitting around afterward. A program is compiled once into JVM bytecode (a .class file, or a runnable .jar), and that compiled output can be executed as many times as you like without paying the compilation cost again. That single difference — compile-once-run-many vs. compile-and-run-together every time — drives most of the advice on when to reach for each.

Kotlin also ships a REPL (just run kotlin with no file argument), which is a third, related but distinct thing: an interactive line-by-line evaluator, useful for quick experiments, but not something you save as a file the way a script or program is.

Syntax

The shapes of a script and a program look almost identical for trivial code, but the rules governing what’s legal at the top level are different:

// script.kts — a Kotlin script: statements run top to bottom, no entry point needed
val message = "Hello"
println(message)

// Program.kt — a Kotlin program: execution starts at fun main()
fun main() {
    val message = "Hello"
    println(message)
}
Aspect Script (.kts) Program (.kt)
Entry point None — top-level statements run in order Required — execution starts at fun main()
Top-level statements Allowed (calls, loops, conditionals directly at file scope) Not allowed — only declarations (val, fun, class, etc.)
Compile & run kotlin script.kts (compiles and runs in one step, every time) kotlinc Main.kt -include-runtime -d app.jar then java -jar app.jar
Command-line args Implicitly available as args Must be declared: fun main(args: Array<String>)
Typical use Automation, exploration, Gradle build files (build.gradle.kts) Applications, libraries, anything run repeatedly or shipped

Examples

Example 1: Hello World, script vs. program

The same greeting, written both ways. The script has nothing but the statement itself:

println("Hello, Kotlin!")

Output:

Hello, Kotlin!

As a compiled program, the exact same println call has to live inside fun main(), because a bare statement isn’t legal at the top level of a .kt file:

fun main() {
    println("Hello, Kotlin!")
}

Output:

Hello, Kotlin!

Both print the identical line. The difference is invisible in the output and entirely about how the file is allowed to be structured and how it gets executed.

Example 2: a small computation, script vs. program

Scripts are great for quick calculations you don’t intend to keep. Here’s one that averages a list of numbers, written as if it were the entire contents of stats.kts:

val numbers = listOf(4, 8, 15, 16, 23, 42)
val average = numbers.average()
println("Average: $average")

Output:

Average: 18.0

average() always returns a Double, which is why the output is 18.0 rather than 18 even though every input was an Int. The same logic promoted to a real program just needs the fun main() wrapper — nothing else changes:

fun main() {
    val numbers = listOf(4, 8, 15, 16, 23, 42)
    val average = numbers.average()
    println("Average: $average")
}

Output:

Average: 18.0

Example 3: command-line arguments

Scripts get command-line arguments for free as an implicit args: Array<String> value visible at the top level — you never declare it yourself. Here’s what the body of args.kts might look like:

if (args.isEmpty()) {
    println("No arguments passed to the script")
} else {
    println("First argument: ${args[0]}")
}

A program has no implicit args — you must declare the parameter explicitly on fun main, and the runtime supplies whatever was typed on the command line when the program was launched:

fun main(args: Array) {
    if (args.isEmpty()) {
        println("No arguments passed to the program")
    } else {
        println("First argument: ${args[0]}")
    }
}

Output (run with no arguments):

No arguments passed to the program

Run either version with myScript.kts hello or java -jar app.jar hello and both would instead print First argument: hello.

How It Works Step by Step

When you run kotlin script.kts: (1) the compiler parses the file and wraps every top-level statement into the constructor body of a generated class; (2) it compiles that class to JVM bytecode in memory (or a temporary location); (3) the JVM instantiates the class immediately; (4) instantiating it runs the constructor, which is your code, in the exact order it was written; (5) the process exits once the last statement finishes. All of this happens on every single invocation — there is no reusable artifact left behind.

When you run a compiled program: (1) kotlinc compiles every .kt file’s declarations into one or more classes (a file-facade class holding top-level functions and properties, plus any explicit classes you wrote), producing .class files (optionally bundled into a runnable .jar with -include-runtime); (2) later, java -jar app.jar (or kotlin MainKt) launches the JVM, which locates the class containing fun main() and invokes it as a static entry point; (3) your code runs from there. Compilation and execution are separate steps, and step 1 only needs to happen once no matter how many times you run step 2.

Common Mistakes

Mistake 1: bare statements at the top level of a real .kt file

Writing script-style code directly in a .kt file doesn’t work, because a plain function call isn’t a declaration:

val x = 10
println(x * 2)

The val x = 10 line is fine on its own — a top-level property is a legal declaration. But println(x * 2) is a bare expression statement, not a declaration, and the compiler rejects it with something like “expecting a top level declaration.” Fix it by moving the statement inside fun main():

val x = 10

fun main() {
    println(x * 2)
}

Output:

20

Mistake 2: defining fun main() inside a script and expecting it to run automatically

Programmers coming from a compiled-program mindset sometimes write a main function inside a .kts file, assuming the script runner will find and call it the way the JVM does for a program:

fun main() {
    println("This will not run automatically")
}

Nothing prints. A script doesn’t search for a function named main; it simply executes its top-level statements in order, and this file’s only top-level statement is a function declaration, which just defines the function without calling it. The fix is to either call the function explicitly (main()) or, more idiomatically for a script, drop the wrapper entirely and put the statement at the top level where it will run immediately:

println("This runs immediately, top to bottom")

Output:

This runs immediately, top to bottom

Best Practices

  • Reach for a script (.kts) for throwaway automation, exploratory calculations, and build configuration — Gradle’s Kotlin DSL files (build.gradle.kts, settings.gradle.kts) are ordinary Kotlin scripts.
  • Reach for a compiled program (fun main()) for anything you’ll run more than a handful of times, hand to someone else, or care about startup performance for — a script recompiles from source on every single invocation, while a program’s bytecode is compiled once and reused.
  • Keep scripts short. Once a script needs multiple files, external dependencies beyond the standard library, or unit tests, that’s a sign it has outgrown script form and belongs in a real Gradle/Kotlin project.
  • On Unix-like systems, give a script a shebang line (#!/usr/bin/env kotlinc -script) and executable permission if you want to invoke it directly as a command-line tool.
  • Remember that args is implicit in a script but must be declared explicitly as fun main(args: Array<String>) in a program — forgetting this is a common source of confusion when porting code between the two forms.
  • Don’t judge a codebase’s quality by whether it uses scripts or programs — they’re different tools for different jobs, not a beginner/advanced split.

Practice Exercises

  • Write the body of a script (as if it were calc.kts) that computes and prints the sum and product of listOf(2, 3, 5, 7). Then rewrite the same logic as a compiled program with fun main(). Expected output for both: Sum: 17, Product: 210.
  • Given a .kt file containing only val greeting = "hi" followed by println(greeting.uppercase()) at the top level, decide whether it compiles as written, and if not, fix it.
  • Write a script body that checks the implicit args array: print "No name given" if it’s empty, otherwise print "Hello, " followed by the first argument. Work out what it prints when run with zero arguments versus one argument, such as world.

Summary

  • A Kotlin script (.kts) executes its top-level statements directly, in order, with no fun main() required.
  • A Kotlin program (.kt) needs a top-level fun main() as its entry point; only declarations, not bare statements, are allowed outside a function body.
  • Scripts are compiled and run together on every invocation; programs are compiled once into reusable bytecode (or a .jar) that can be executed repeatedly without recompiling.
  • Command-line arguments are implicitly available as args in a script, but must be declared explicitly via fun main(args: Array<String>) in a program.
  • Use scripts for quick, disposable tasks and build configuration; use compiled programs for anything meant to be reused, shipped, or run often.