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
argsis implicit in a script but must be declared explicitly asfun 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 oflistOf(2, 3, 5, 7). Then rewrite the same logic as a compiled program withfun main(). Expected output for both:Sum: 17, Product: 210. - Given a
.ktfile containing onlyval greeting = "hi"followed byprintln(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
argsarray: 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 asworld.
Summary
- A Kotlin script (
.kts) executes its top-level statements directly, in order, with nofun main()required. - A Kotlin program (
.kt) needs a top-levelfun 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
argsin a script, but must be declared explicitly viafun 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.
