LearnAI ToolsCareerPractice BuildsPlayDevelopmentContact
KotlinBeginner~1.5 hours

Student Grading System

Process a list of students with nullable marks and produce grade summaries using null safety and when.

Null Safetywhen ExpressionsFunctions

Overview

Real student data is never perfectly complete — a mark might genuinely not exist yet because a student has not sat an exam, transferred in mid-term, or been granted a deferral. Kotlin's type system makes that distinction explicit at compile time: val marks: Int can never be missing, but val marks: Int? can, and the compiler will not let you use a nullable value as if it were guaranteed to exist without first proving you have handled the null case. That single character, ?, is the difference between a NullPointerException waiting to happen and a program the compiler has already verified is safe.

By the end of this tutorial you will have a console application that processes a list of Student records with genuinely nullable marks: Int?, converts each present score into a letter grade with a when expression over a range, and reports "No marks recorded" for anyone still missing a score — all without a single manual null check written as if (x != null). Instead you will use Kotlin's safe-call operator ?., the elvis operator ?:, and the scope function let together, which is the idiomatic combination for "do something with this value only if it exists, otherwise fall back to a default."

What You'll Build
  • A data class Student(val name: String, val marks: Int?) with genuinely nullable marks.
  • A gradeFor(score: Int) function that maps a 0-100 score to a letter grade with a when expression over ranges.
  • A gradeSummary(student: Student) function combining ?.let { } and ?: to safely format a result.
  • A classAverage(students: List<Student>) function that skips nullable marks with mapNotNull.
  • A printReport() function that prints one grade line per student, including those with no marks.
  • A small hard-coded roster driving the program from main().

Prerequisites

  • Data classes — the data class keyword and constructor properties.
  • Nullable types — the ? suffix on a type, and why Kotlin requires a value to be proven non-null before it is used unsafely.
  • Null safety operators — the safe call ?., the elvis operator ?:, and the scope function let.
  • The when expression, including matching against a numeric range with in.
  • Function basics — parameters, return types, and Kotlin's single-expression function syntax.

Project Structure

The whole program lives in a single file, StudentGradingSystem.kt, containing one data class, Student, three top-level functions (gradeFor, gradeSummary, classAverage), and fun main(). Keeping gradeFor and gradeSummary as free functions rather than methods on Student is a deliberate choice: Student itself stays a pure, minimal data holder, while the grading logic — which could plausibly change independently of what a student record even looks like — lives separately and is easy to test or swap out on its own.

Step 1: Define the Student Data Class With Nullable Marks

The key design decision in this whole project is val marks: Int? instead of val marks: Int. That trailing ? is not decoration — it changes marks's type to "either an Int, or the absence of one," and from this point forward the Kotlin compiler refuses to compile any code that treats student.marks as a plain Int without first narrowing it, forcing every later step to deal with the missing case explicitly instead of letting it slip through as an accidental crash.

// marks is Int? (nullable), not Int, because a real student roster can have
// students who have not been marked yet — the type itself documents that a
// missing score is an expected, ordinary case, not an error condition.
data class Student(
val name: String, // Student's full name
val marks: Int? // Nullable: null means "no marks recorded yet" for this student
)

Step 2: Map a Score to a Letter Grade With when

gradeFor() takes a plain, non-nullable Int — by the time a score reaches this function, the caller in Step 3 will already have proven it is not null, so gradeFor itself never has to think about nullability at all. The when expression matches ranges with the in operator, and because every branch returns a String, the whole when block evaluates to a String that the function can return directly with Kotlin's single-expression = when (...) { ... } syntax.

// A single-expression function: the body is one when expression, so "=" is
// used instead of a { return ... } block. score is a plain Int here, never
// nullable — by the time this is called, the caller has already handled null.
fun gradeFor(score: Int): String = when (score) {
in 90..100 -> "A" // "in" tests range membership; matches 90 through 100 inclusive
in 80..89 -> "B"
in 70..79 -> "C"
in 60..69 -> "D"
in 0..59 -> "F"
else -> "Invalid" // Anything outside 0-100 is not a real percentage score
}
Example Usage

Click Run to see what this code prints.

Step 3: Handle Missing Marks With Safe Calls and Elvis

gradeSummary() is where the null-safety idiom comes together in one line: student.marks?.let { gradeFor(it) } only runs the lambda — and only calls gradeFor(it) — when student.marks is non-null; if it is null, the whole ?.let { } expression short-circuits to null without ever calling gradeFor. The ?: (elvis operator) immediately after supplies what to use instead when that happens: "No marks recorded". Together, marks?.let { gradeFor(it) } ?: "No marks recorded" reads almost like the English sentence it represents, and there is no if (marks != null) anywhere in it.

fun gradeSummary(student: Student): String {
// ?. (safe call): only enters the lambda if marks is non-null.
// let { }: runs the lambda with "it" bound to the non-null Int.
// ?: (elvis): supplies the fallback string when the left side was null.
val grade = student.marks?.let { gradeFor(it) } ?: "No marks recorded"
return "${student.name}: $grade"
}
Example Usage

Click Run to see what this code prints.

Step 4: Summarize a Class of Students

classAverage() needs to compute an average over only the students who actually have marks, silently excluding anyone with null. students.mapNotNull { it.marks } does exactly that in one call: it maps each student to their marks, then drops every null result, leaving a plain List<Int> with no nullability left to worry about at all. average() on an empty list returns Double.NaN rather than throwing, so the function still guards the empty-list case explicitly for a cleaner "N/A" result.

fun classAverage(students: List<Student>): Double? {
val scores = students.mapNotNull { it.marks } // Keeps only the non-null marks; nulls are silently filtered out
if (scores.isEmpty()) {
return null // No marked students at all; the caller decides how to display that
}
return scores.average() // average() over a confirmed non-empty List<Int>
}

Step 5: Build the Program Entry Point

main() builds a small hard-coded roster mixing students who have marks with one who does not, then prints a summary line per student with gradeSummary() from Step 3, followed by the class average from Step 4 — using the same ?.let { } ?: pattern one more time to format a Double? only when it is actually present.

fun main() {
val roster = listOf(
Student("Ananya Rao", 88),
Student("Vikram Shah", null), // No marks recorded for this student yet
Student("Priya Nair", 95),
Student("Rohan Verma", 42),
)
println("===== STUDENT GRADING SYSTEM =====")
for (student in roster) {
println(gradeSummary(student)) // Reuses Step 3's null-safe summary for every student, uniformly
}
val average = classAverage(roster)
// Formats the average only if one could be computed; falls back to "N/A" otherwise, same ?.let/?: idiom as Step 3
println("\nClass average: ${average?.let { "%.2f".format(it) } ?: "N/A"}")
}

Complete Code

Here is the full program with every declaration assembled in the correct order, ready to save as StudentGradingSystem.kt and run with kotlinc StudentGradingSystem.kt -include-runtime -d grading.jar && java -jar grading.jar.

data class Student(
val name: String,
val marks: Int?
)
fun gradeFor(score: Int): String = when (score) {
in 90..100 -> "A"
in 80..89 -> "B"
in 70..79 -> "C"
in 60..69 -> "D"
in 0..59 -> "F"
else -> "Invalid"
}
fun gradeSummary(student: Student): String {
val grade = student.marks?.let { gradeFor(it) } ?: "No marks recorded"
return "${student.name}: $grade"
}
fun classAverage(students: List<Student>): Double? {
val scores = students.mapNotNull { it.marks }
if (scores.isEmpty()) {
return null
}
return scores.average()
}
fun main() {
val roster = listOf(
Student("Ananya Rao", 88),
Student("Vikram Shah", null),
Student("Priya Nair", 95),
Student("Rohan Verma", 42),
)
println("===== STUDENT GRADING SYSTEM =====")
for (student in roster) {
println(gradeSummary(student))
}
val average = classAverage(roster)
println("\nClass average: ${average?.let { "%.2f".format(it) } ?: "N/A"}")
}

Sample Run

Sample Run

Click Run to see what this code prints.

Extend This Project

  • Add a List<Int>? field for multiple assessment scores per student, and average them with it.scores?.average() before grading.
  • Write a topStudent(students: List<Student>): Student? function using students.maxByOrNull { it.marks ?: -1 } so students with no marks never win.
  • Add a attendance: Int? field and combine it with marks using nested ?.let { } calls to compute a combined eligibility flag.
  • Replace the letter-grade when with one that also returns a Pair<String, String> of grade and remark ("A" to "Excellent", etc.).
  • Read the roster from user input with readLine() instead of a hard-coded list, treating a blank line as null marks.

Summary

You built a student grading system where a nullable Int? makes "no marks yet" a case the compiler forces you to handle, not a runtime surprise, and the combination of ?., let, and ?: let you express that handling in a single readable line instead of an if (x != null) block. The when-over-a-range pattern in gradeFor() and the mapNotNull filtering in classAverage() are both idioms you will reach for constantly in Kotlin — anywhere you need to classify a value into buckets, or anywhere a collection can contain nulls you need to safely ignore.