Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Scala 3  Module 1 – Start here: Scala 3.9 LTS on the JVM

Scala 3 for Java and Python developers

Map Java classes, records, static members, switch and Optional, and Python data classes, comprehensions and None, to Scala 3, and avoid five early traps.

  • Beginner
  • 15 minutes
  • Examples run with scala 3.9.0, Java 25.0.4.1 and Python 3.14.8
  • By MySmartCoPilot
Free: 15 minutes

What you will learn

  • Map Java classes, records, static members, switch, Optional, streams and checked exceptions to Scala
  • Map Python data classes, comprehensions, None and duck typing to Scala
  • Rewrite a for-comprehension as the withFilter, map and flatMap calls it stands for
  • Avoid five first-week traps, from == against eq and Unit to nulls from Java, immutable lists and Int overflow

Before you start

What you’ll learn · Before you start

What you will learn

  • Map Java classes, records, static members, switch, Optional, streams and checked exceptions to Scala
  • Map Python data classes, comprehensions, None and duck typing to Scala
  • Rewrite a for-comprehension as the withFilter, map and flatMap calls it stands for
  • Avoid five first-week traps, from == against eq and Unit to nulls from Java, immutable lists and Int overflow

Before you start

On this page

The same order total takes 53 lines of Java, 33 of Scala and 47 of Python in this lesson, and all three programs print the same eight lines. Most of what you know carries over, because classes, lists, maps and if exist in Scala too. What changes is where things live and what counts as a value: there is no static, a match returns a value, and a value that may be missing is an Option rather than null or None.

This lesson maps Java and Python to Scala side by side, shows what a for-comprehension turns into, and walks through five traps that catch experienced developers in their first week of Scala.

One order total in three languages

Here is the program three times: an order of three lines, a discount code that may be unknown, and shipping that depends on the method. Switch between the tabs; the Python version ran on Python 3.14.8, and its output is in its own tab:

Order totals in Java, Scala and Python

Java · invoice-java/InvoiceTotals.java

import java.util.List;
import java.util.Optional;

// An order's totals in cents: line totals, a discount code that may be unknown, and shipping by method.
public class InvoiceTotals {
    record Line(String item, int quantity, long unitCents) {
        long totalCents() {
            return quantity * unitCents;
        }
    }

    enum Shipping { STANDARD, EXPRESS, PICKUP }

    static long shippingCents(Shipping method) {
        return switch (method) {
            case STANDARD -> 499;
            case EXPRESS -> 1299;
            case PICKUP -> 0;
        };
    }

    static Optional<Integer> discountPercent(String code) {
        return switch (code) {
            case "WELCOME10" -> Optional.of(10);
            case "BULK5" -> Optional.of(5);
            default -> Optional.empty();
        };
    }

    static String money(long cents) {
        return String.format("%d.%02d", cents / 100, cents % 100);
    }

    public static void main(String[] args) {
        List<Line> lines = List.of(
            new Line("Notebook", 3, 450),
            new Line("Pen", 10, 120),
            new Line("Stapler", 1, 1599));
        for (Line line : lines) {
            System.out.printf("%-9s %3d x %6s = %7s%n", line.item(), line.quantity(), money(line.unitCents()), money(line.totalCents()));
        }
        long subtotal = lines.stream().mapToLong(Line::totalCents).sum();
        String code = "WELCOME10";
        int percent = discountPercent(code).orElse(0);
        long discount = subtotal * percent / 100;
        long shipping = shippingCents(Shipping.EXPRESS);
        System.out.printf("%-24s %7s%n", "Subtotal", money(subtotal));
        System.out.printf("%-24s %7s%n", "Discount " + code + " (" + percent + "%)", "-" + money(discount));
        System.out.printf("%-24s %7s%n", "Shipping, express", money(shipping));
        System.out.printf("%-24s %7s%n", "Total", money(subtotal - discount + shipping));
        System.out.println("Code SPRING: " + discountPercent("SPRING").map(p -> p + "%").orElse("no discount"));
    }
}

Scala · invoice-scala/InvoiceTotals.scala

// The same order in Scala 3: a case class for a line, an enum, match expressions and an Option.
case class Line(item: String, quantity: Int, unitCents: Long):
  def totalCents: Long = quantity * unitCents

enum Shipping:
  case Standard, Express, Pickup

def shippingCents(method: Shipping): Long = method match
  case Shipping.Standard => 499
  case Shipping.Express  => 1299
  case Shipping.Pickup   => 0

def discountPercent(code: String): Option[Int] = code match
  case "WELCOME10" => Some(10)
  case "BULK5"     => Some(5)
  case _           => None

def money(cents: Long): String = f"${cents / 100}%d.${cents % 100}%02d"

@main def invoiceTotals(): Unit =
  val lines = List(Line("Notebook", 3, 450), Line("Pen", 10, 120), Line("Stapler", 1, 1599))
  for line <- lines do
    println(f"${line.item}%-9s ${line.quantity}%3d x ${money(line.unitCents)}%6s = ${money(line.totalCents)}%7s")
  val subtotal = lines.map(_.totalCents).sum
  val code = "WELCOME10"
  val percent = discountPercent(code).getOrElse(0)
  val discount = subtotal * percent / 100
  val shipping = shippingCents(Shipping.Express)
  println(f"${"Subtotal"}%-24s ${money(subtotal)}%7s")
  println(f"${s"Discount $code ($percent%)"}%-24s ${"-" + money(discount)}%7s")
  println(f"${"Shipping, express"}%-24s ${money(shipping)}%7s")
  println(f"${"Total"}%-24s ${money(subtotal - discount + shipping)}%7s")
  println("Code SPRING: " + discountPercent("SPRING").map(p => s"$p%").getOrElse("no discount"))

Python · invoice_totals.py

# The same order in Python: a frozen dataclass, a generator expression, a match statement and None.
from dataclasses import dataclass


@dataclass(frozen=True)
class Line:
    item: str
    quantity: int
    unit_cents: int

    def total_cents(self) -> int:
        return self.quantity * self.unit_cents


def shipping_cents(method: str) -> int:
    match method:
        case "STANDARD":
            return 499
        case "EXPRESS":
            return 1299
        case "PICKUP":
            return 0
    raise ValueError(method)


def discount_percent(code: str) -> int | None:
    return {"WELCOME10": 10, "BULK5": 5}.get(code)


def money(cents: int) -> str:
    return f"{cents // 100}.{cents % 100:02d}"


lines = [Line("Notebook", 3, 450), Line("Pen", 10, 120), Line("Stapler", 1, 1599)]
for line in lines:
    print(f"{line.item:<9} {line.quantity:3d} x {money(line.unit_cents):>6} = {money(line.total_cents()):>7}")
subtotal = sum(line.total_cents() for line in lines)
code = "WELCOME10"
percent = discount_percent(code) or 0
discount = subtotal * percent // 100
shipping = shipping_cents("EXPRESS")
print(f"{'Subtotal':<24} {money(subtotal):>7}")
print(f"{f'Discount {code} ({percent}%)':<24} {'-' + money(discount):>7}")
print(f"{'Shipping, express':<24} {money(shipping):>7}")
print(f"{'Total':<24} {money(subtotal - discount + shipping):>7}")
found = discount_percent("SPRING")
print("Code SPRING: " + (f"{found}%" if found is not None else "no discount"))

Output

Notebook    3 x   4.50 =   13.50
Pen        10 x   1.20 =   12.00
Stapler     1 x  15.99 =   15.99
Subtotal                   41.49
Discount WELCOME10 (10%)   -4.14
Shipping, express          12.99
Total                      50.34
Code SPRING: no discount

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 invoice_totals.py

The Java and Scala versions ran on the same JDK 25. Java’s launcher compiles a single source file in memory and runs it, so java InvoiceTotals.java needs no separate javac step:

The Java and Scala versions, run Python · invoice_session.py
from session import run

# The Java version with the JDK's single-file launcher, then the Scala version, on the same JDK 25.
run("""
cd invoice-java
java InvoiceTotals.java
cd ../invoice-scala
scala run . --server=false
""")

Output

$ cd invoice-java
$ java InvoiceTotals.java
Notebook    3 x   4.50 =   13.50
Pen        10 x   1.20 =   12.00
Stapler     1 x  15.99 =   15.99
Subtotal                   41.49
Discount WELCOME10 (10%)   -4.14
Shipping, express          12.99
Total                      50.34
Code SPRING: no discount
$ cd ../invoice-scala
$ scala run . --server=false
Notebook    3 x   4.50 =   13.50
Pen        10 x   1.20 =   12.00
Stapler     1 x  15.99 =   15.99
Subtotal                   41.49
Discount WELCOME10 (10%)   -4.14
Shipping, express          12.99
Total                      50.34
Code SPRING: no discount

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 invoice_session.py

The three outputs are the same, line for line. Put the versions side by side and the pieces match one to one: a Java record, a Scala case class: A class for immutable data. The compiler adds equals and hashCode that compare the fields, a readable toString, a copy method and support for pattern matching. and a frozen Python data class; an enum of shipping methods in Java and in Scala, where Python uses plain strings; three ways of choosing, switch, match and Python’s match statement; and a discount that may be missing, an Optional, an Option: A container that holds either one value, Some(value), or nothing, None. Scala code uses it instead of null for a value that may be missing. and None. The next two sections go through the differences that matter.

Checkpoint

Check: One program, three languages

  1. One question For an unknown code, Java's discountPercent returns Optional.empty(). What do the Scala and the Python versions return?

    Choose one answer.

    A hint

    Each version marks a discount that may be missing in its own way.

    Show the answer: One question

    Answer: None, the empty Option, in Scala; None in Python

    The three versions match piece for piece: Optional, Option and None mark a discount that may be missing, and each caller turns it into 0, with orElse(0) in Java, getOrElse(0) in Scala and or 0 in Python.

From Java: what replaces static, records, switch and checked exceptions

Most Java constructs have a direct Scala counterpart. The table lists the ones every Java developer meets in the first week:

In Java In Scala What changes
a class with fields and a constructor class Line(item: String, quantity: Int) the parameter list after the name is the constructor
record Line(...) case class Line(...) equals, hashCode, toString, copy and pattern matching come with it
static members members of an object: A singleton: a class with exactly one instance, created when it is first used. The members of an object play the role of Java's static members., often the class’s companion object there is no static keyword
switch match an expression with a value; on an enum, a missing case is warning E029
Optional<T> Option[T]: Some(x) or None used across the standard library, and Option(x) turns a Java null into None
streams methods of the collections: map, filter, sum each call builds its result at once; no stream() or collect()
checked exceptions, throws no checked exceptions @throws tells Java callers what a Scala method can throw
void Unit: The type of expressions that run for their effect, such as println. Its only value is written (), and it plays the role of void in Java. a real type whose only value is ()
final local variable val var is the variable you can reassign

Two rows deserve a second look. A Java switch expression over an enum must cover every value, and javac refuses one that does not, while a Scala match only warns, as the previous lesson showed with E029 and a MatchError. Here is Java’s side of it, on the same JDK 25:

switch-java/Fee.java Java · switch-java/Fee.java
// A Java switch expression over an enum, with one case missing.
public class Fee {
    enum Payment { CARD, WALLET, CASH }

    static long feeCents(Payment p) {
        return switch (p) {
            case CARD -> 30;
            case WALLET -> 10;
        };
    }

    public static void main(String[] args) {
        System.out.println(feeCents(Payment.CASH));
    }
}
Java refuses a switch that misses a value Python · switch_session.py
from session import run

# Java refuses a switch expression that misses a value of its enum; it reports the problem before anything runs.
run("""
cd switch-java
!java Fee.java
""")

Output

$ cd switch-java
$ java Fee.java
Fee.java:6: error: the switch expression does not cover all possible input values
        return switch (p) {
               ^
1 error
error: compilation failed
[exit status 1]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 switch_session.py

The other is checked exceptions. Scala does not check them, so a Scala method never declares what it throws, and Java code that calls it is not forced to catch anything. When Java callers should be told, annotate the method with @throws(classOf[java.io.IOException]); Java then treats it as a method that declares throws IOException.

Keep -Werror on for Java's habit

Java developers rely on the compiler to refuse a switch expression with a missing case. Compile Scala with -Werror, as the previous lesson showed, and a missing case in a match on an enum fails the build in the same way.

Checkpoint

Check: Java constructs in Scala

  1. Question 1 of 2 Where does a Java static method go when you port the class to Scala?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Into an object, usually the companion object of the class

    An object is a singleton, created when first used, and its members are what a Java developer would make static. An object with the same name as a class is its companion and can see the class's private members.

  2. Question 2 of 2 A Java record record Line(String item, int quantity) becomes which Scala declaration, with equals, hashCode and toString included?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: case class Line(item: String, quantity: Int)

    Case classes cover what Java records do, and add copy, which builds a changed copy, and pattern matching on their fields.

From Python: data classes, comprehensions, None and duck typing

Python developers find the indentation familiar: Scala 3 can drop braces and marks blocks by indentation, with then and do after conditions. The differences are in the types, which Scala checks before the program runs:

In Python In Scala What changes
@dataclass(frozen=True) case class the Scala 3 Book calls case classes similar to frozen data classes; copy plays the part of replace
[f(x) for x in xs if p(x)] for x <- xs if p(x) yield f(x) the result keeps the collection’s type, and the comprehension is a chain of method calls
None, which any name may hold Option[A]: Some(a) or None the type says that a value may be missing, and the compiler makes you handle it
duck typing traits, checked by the compiler a missing method is a compile error rather than an AttributeError at run time
int, which never overflows Int (32 bits), Long (64 bits), BigInt past its limit an Int wraps around, so money is counted in Long
a list you append to an immutable List; ArrayBuffer when you must mutate :+ returns a new list and leaves the old one unchanged

The discount_percent function of the Python version shows the biggest change in practice. In Python it returns an int or None, and nothing stops a caller from adding the None to a number. In Scala it returns Option[Int], and discountPercent(code) + 1 does not compile: the caller must say what happens when there is no discount, with getOrElse(0) as the order total does, or with a match.

Key idea

The Scala version of a Java or Python program has the same parts, but its types carry more: a value that may be missing is an Option, a choice is a match with a value, and shared members live in an object.

JSON to Code Generator Paste a JSON sample and get a Scala 3 case class for it, or a Python data class for the same data.

Checkpoint

Check: Python constructs in Scala

  1. Question 1 of 2 What happens when you compile this line, given that discountPercent returns Option[Int]?

    Read the code, then choose one answer.

    val percent = discountPercent("SPRING") + 1
    Show the answer: Question 1 of 2

    Answer: It does not compile, because an Option[Int] cannot be added to an Int; write discountPercent("SPRING").getOrElse(0) + 1

    In Python, discount_percent returns an int or None and a caller can forget the None. In Scala the Option[Int] makes the case visible: handle it with getOrElse, a match or map.

  2. Question 2 of 2 In Python, calling total_cents() on an object that lacks it fails at run time with AttributeError. What happens in Scala when you call a method that the type does not have?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: The compiler rejects the call, so the program is never built

    Where Python relies on duck typing, Scala uses traits and types that the compiler checks, so the error moves from a run in production to the compile on your computer.

A for-comprehension is withFilter, map and flatMap

A for-comprehension: A for expression with yield. The compiler rewrites it into map, flatMap and withFilter calls, so it works for lists, Option, Either and any other type with those methods. looks like a Python comprehension, but it is shorthand for method calls. The language specification gives the rules: a generator with yield becomes map, a second generator makes the first one a flatMap, and a guard if becomes withFilter. The program prints each comprehension next to the calls it stands for:

fors/Fors.scala Scala · fors/Fors.scala
// A for-comprehension is rewritten into withFilter, map and flatMap, so it works on any type that has them.
@main def fors(): Unit =
  val marks = List(72, 35, 91, 48)
  val doubled = for m <- marks if m >= 40 yield m * 2
  val same = marks.withFilter(m => m >= 40).map(m => m * 2)
  println(s"for/yield:      $doubled")
  println(s"rewritten:      $same")

  val sizes = List("S", "M")
  val colours = List("red", "blue")
  val pairs = for s <- sizes; c <- colours yield s"$s-$c"
  val pairs2 = sizes.flatMap(s => colours.map(c => s"$s-$c"))
  println(s"two generators: $pairs")
  println(s"rewritten:      $pairs2")

  def price(code: String): Option[Int] = Map("A1" -> 450, "B2" -> 120).get(code)
  println(s"both found:     ${for x <- price("A1"); y <- price("B2") yield x + y}")
  println(s"one missing:    ${for x <- price("A1"); y <- price("Z9") yield x + y}")
Comprehensions and the calls they stand for Python · fors_session.py
from session import run

# Each for-comprehension next to the calls the compiler rewrites it into.
run("""
cd fors
scala run . --server=false
""")

Output

$ cd fors
$ scala run . --server=false
for/yield:      List(144, 182, 96)
rewritten:      List(144, 182, 96)
two generators: List(S-red, S-blue, M-red, M-blue)
rewritten:      List(S-red, S-blue, M-red, M-blue)
both found:     Some(570)
one missing:    None

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 fors_session.py

Each pair printed the same result. Step through the second one, two generators, as the compiler rewrites it:

Interactive · stage by stage How the compiler rewrites two generators Step through the rewrite of for s <- sizes; c <- colours yield s"$s-$c", from the comprehension to the list it returns.
How the compiler rewrites two generators, step 5 of 5. Showing The for-comprehension, sizes.flatMap(s => …), colours.map(c => …), The yield expression, A list of four strings. In focus: A list of four strings.The for-comprehensiontwo generators, one yieldsizes.flatMap(s => …)the first generatorcolours.map(c => …)the last generatorThe yield expressions"$s-$c" for each pairA list of four stringsS-red, S-blue, M-red …joined by flatMap

The drawing shows the whole scene; its 5 steps are listed below.

Every step as text
  1. The comprehension. The program writes for s <- sizes; c <- colours yield s"$s-$c": every size with every colour.
  2. The first generator. Because another generator follows it, the first one becomes a flatMap over sizes. For each size it expects a whole list back, and flatMap joins those lists.
  3. The last generator. The last generator becomes a map over colours, nested inside the flatMap, so it runs once for every size.
  4. The yield. What follows yield becomes the body of the innermost function: s"$s-$c" for the current size and colour.
  5. The result. map gives two strings for each size, and flatMap joins them into one list, List(S-red, S-blue, M-red, M-blue), the same as the session printed for both forms.

Because a for-comprehension is only calls, it works on every type that has map, flatMap and withFilter, not just on collections. The last two lines use it on Option: when both prices exist, the result is Some(570); when one is missing, the whole result is None, with no null check anywhere. The same shape works for Either and Future, which later lessons of this track use.

Checkpoint

Check: What a for-comprehension becomes

  1. Question 1 of 2 Which calls does this for-comprehension stand for?

    Read the code, then choose one answer.

    for m <- marks if m >= 40 yield m * 2
    Show the answer: Question 1 of 2

    Answer: marks.withFilter(m => m >= 40).map(m => m * 2)

    The specification folds a guard into its generator with withFilter, and turns a generator with yield into map. The session printed the same list, List(144, 182, 96), for both forms.

  2. Question 2 of 2 price("A1") is Some(450) and price("Z9") is None. What does for x <- price("A1"); y <- price("Z9") yield x + y give?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: None

    The comprehension is price("A1").flatMap(x => price("Z9").map(y => x + y)). Mapping over None gives None, so one missing value makes the result None, as the session printed.

Five traps of the first week

Each of these traps compiles without a complaint and then prints something a Java or Python developer does not expect. The program shows all five:

traps/Traps.scala Scala · traps/Traps.scala
// Five things that surprise Java and Python developers in their first week of Scala.
case class Money(cents: Long)

def log(message: String): Unit = println(s"log: $message")

@main def traps(): Unit =
  // 1. == compares values (it calls equals, and handles null); eq compares references.
  val a = Money(450)
  val b = Money(450)
  println(s"a == b: ${a == b}, a eq b: ${a eq b}")
  val missing: Money = null
  println(s"missing == a: ${missing == a}")

  // 2. Unit is a real value, the only one of its type, written ().
  val result: Unit = log("saved")
  println(s"result: $result")

  // 3. A Java method may return null; Option(...) turns null into None.
  val home = System.getProperty("app.home")
  println(s"raw: $home, as an Option: ${Option(home)}")
  println(s"with a default: ${Option(home).getOrElse("/opt/app")}")

  // 4. List is immutable: :+ returns a new list and leaves the old one as it was.
  val prices = List(450, 120)
  val more = prices :+ 1599
  println(s"prices: $prices, more: $more")

  // 5. Int has 32 bits, unlike Python's int: past its maximum it wraps around.
  val biggest = Int.MaxValue
  println(s"Int.MaxValue + 1 = ${biggest + 1}, as a Long: ${biggest.toLong + 1}")
Five first-week traps Python · traps_session.py
from session import run

# Five first-week traps, each printed next to what it shows.
run("""
cd traps
scala run . --server=false
""")

Output

$ cd traps
$ scala run . --server=false
a == b: true, a eq b: false
missing == a: false
log: saved
result: ()
raw: null, as an Option: None
with a default: /opt/app
prices: List(450, 120), more: List(450, 120, 1599)
Int.MaxValue + 1 = -2147483648, as a Long: 2147483648

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 traps_session.py

  1. == compares values. In Scala a == b calls equals, and it is safe with null: the API documentation defines x == that as if (x eq null) that eq null else x.equals(that). So two Money(450) values are equal, and missing == a is false instead of a NullPointerException. reference equality: Two names that point to the same object in memory. In Scala, a eq b tests it, while a == b calls equals and compares values., Java’s == on objects and Python’s is, is eq in Scala, and it was false for the two separate objects.
  2. Unit is a value. A method that returns Unit, like log, still returns something: (), the one value of Unit, as result: () shows. Where Java has void, Scala has an ordinary type.
  3. Java can hand you null. System.getProperty("app.home") returned null because the property is not set. Option(home) turns null into None and anything else into Some, and getOrElse then supplies the default. Wrap values from Java where they enter your Scala code.
  4. A List never changes. prices :+ 1599 returned a new list with three items and left prices with two. Code written in the Java or Python habit, calling :+ and ignoring the result, loses the item.
  5. An Int overflows. Int.MaxValue + 1 printed -2147483648: an Int has 32 bits and wraps around, while a Python int would just grow. Convert to Long first, as the last value shows, or use BigInt.

Comparing values with eq

eq asks whether two names point to the same object, so two equal strings or case class values can give false, as a eq b did. Compare values with ==, and keep eq for the rare case where identity matters.

Checkpoint

Check: Five first-week traps

  1. Question 1 of 2 a and b are two separate Money(450) values of a case class. What are a == b and a eq b?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: a == b is true, and a eq b is false

    The session printed a == b: true, a eq b: false. Use == for values and keep eq for the rare code that needs identity.

  2. Question 2 of 2 What does this print?

    Read the code, then choose one answer.

    val prices = List(450, 120)
    prices :+ 1599
    println(prices.size)
    Show the answer: Question 2 of 2

    Answer: 2

    Keep the result of :+, as in val more = prices :+ 1599, or use a mutable buffer when you really need to change a collection in place.

Practise: port a Java method to Scala

Here is a small Java method to port: a letter grade for a mark from 0 to 100, or none for a mark outside that range. Its Scala starter has the same checks as the Java main, and its grade returns None for now:

grades-java/Grades.java Java · grades-java/Grades.java
import java.util.List;
import java.util.Optional;

// The Java original to port: a letter grade for a mark from 0 to 100, or none when the mark is out of range.
public class Grades {
    static Optional<String> grade(int mark) {
        if (mark < 0 || mark > 100) {
            return Optional.empty();
        }
        return switch (mark / 10) {
            case 10, 9 -> Optional.of("A");
            case 8 -> Optional.of("B");
            case 7 -> Optional.of("C");
            case 6 -> Optional.of("D");
            default -> Optional.of("F");
        };
    }

    public static void main(String[] args) {
        for (int mark : List.of(95, 83, 71, 64, 12, 101)) {
            System.out.println(mark + " -> " + grade(mark).orElse("no grade"));
        }
    }
}
grades/Grades.scala Scala · grades/Grades.scala
// Practise: port grade from Grades.java. Return Some(letter) for a mark from 0 to 100, and None otherwise.
def grade(mark: Int): Option[String] = None

@main def grades(): Unit =
  val expected = List(95 -> Some("A"), 83 -> Some("B"), 71 -> Some("C"), 64 -> Some("D"), 12 -> Some("F"), 101 -> None)
  val results =
    for (mark, want) <- expected yield
      val got = grade(mark)
      println(s"${if got == want then "ok  " else "FAIL"} $mark -> ${got.getOrElse("no grade")}")
      got == want
  println(s"${results.count(ok => ok)} of ${results.size} checks pass")
  if results.contains(false) then sys.exit(1)

The session runs the Java original, then the starter, which passes only the one check that expects no grade:

The Java original and the Scala starter Python · grades_session.py
from session import run

# The Java original, then the Scala starter of the exercise, whose grade always returns None.
run("""
cd grades-java
java Grades.java
cd ../grades
!scala run . --server=false
""")

Output

$ cd grades-java
$ java Grades.java
95 -> A
83 -> B
71 -> C
64 -> D
12 -> F
101 -> no grade
$ cd ../grades
$ scala run . --server=false
FAIL 95 -> no grade
FAIL 83 -> no grade
FAIL 71 -> no grade
FAIL 64 -> no grade
FAIL 12 -> no grade
ok   101 -> no grade
1 of 6 checks pass
[exit status 1]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 grades_session.py

Copy grades/Grades.scala into an empty folder, write grade so that scala run . reports 6 of 6 checks pass, and compare your version with the Java one: which lines did the match replace? First hint: start with an if that returns None outside 0 to 100, then match on mark / 10, where case 10 | 9 => Some("A") covers two values in one case.

Then test the five ideas of the lesson in a short session of questions, from the Java and Python maps to the five traps; each answer is explained the moment you give it:

Practice that adapts to you

Practise this lesson

14 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 14 In the three order programs, which Scala construct plays the part of the Java record and of the frozen Python data class?

    Choose one answer.

    Show the answer: Question 1 of 14

    Answer: A case class

    All three programs model a line as immutable data with named fields and a method: record, case class and frozen data class.

  2. Question 2 of 14 How did the session run the Java version without a separate javac step?

    Choose one answer.

    Show the answer: Question 2 of 14

    Answer: java InvoiceTotals.java compiles the one source file in memory and runs it

    Java's source-file mode compiles and runs a single file in one command, which makes small Java examples as quick to run as scala run.

  3. Question 3 of 14 What does Scala do with Java's checked exceptions?

    Choose one answer.

    Show the answer: Question 3 of 14

    Answer: It does not check them; @throws tells Java callers what a Scala method throws

    Without checked exceptions, a Scala method is not forced to declare what it throws. When Java code calls it and should catch an IOException, annotate the method with @throws(classOf[java.io.IOException]).

  4. Question 4 of 14 A switch expression in Java and a match in Scala, each over an enum of three payment methods, miss one value. What happens?

    Choose one answer.

    Show the answer: Question 4 of 14

    Answer: javac refuses the switch expression, while Scala compiles the match with warning E029

    Java's switch expression must cover every value. Scala's match is checked too, but a gap is a warning, so compile with -Werror to get Java's strictness.

  5. Question 5 of 14 Which Scala line does what this Java line does?

    Read the code, then choose one answer.

    long subtotal = lines.stream().mapToLong(Line::totalCents).sum();
    Show the answer: Question 5 of 14

    Answer: val subtotal = lines.map(_.totalCents).sum

    Each Scala collection call builds its result at once: map gives a list of totals, and sum adds them. The Scala version of the order program uses exactly this line.

  6. Question 6 of 14 Which Scala declaration is closest to Python's @dataclass(frozen=True)?

    Choose one answer.

    Show the answer: Question 6 of 14

    Answer: A case class

    The Scala 3 Book says case classes are similar to Python's frozen data classes. Both are immutable and come with equality and a readable string form.

  7. Question 7 of 14 How do you write the Python list [x * 2 for x in marks if x >= 40] in Scala, for a List of marks?

    Choose one answer.

    Show the answer: Question 7 of 14

    Answer: for x <- marks if x >= 40 yield x * 2

    A for-comprehension with yield builds a collection like a list comprehension, and its result keeps the type of the collection it reads, here a List.

  8. Question 8 of 14 A Python developer adds up 3,000,000 amounts of 1,000 cents each as a Scala List[Int] with sum. What is the result?

    Choose one answer.

    Show the answer: Question 8 of 14

    Answer: -1294967296, because the Int total wraps around after 2,147,483,647

    An Int wraps around past 2,147,483,647, as Int.MaxValue + 1 showed in the session. Count money in Long, whose limit is about 9.2 × 10¹⁸, or in BigInt.

  9. Question 9 of 14 Which method does a guard, the if inside a for-comprehension, become?

    Choose one answer.

    Show the answer: Question 9 of 14

    Answer: withFilter

    A generator becomes map, a following generator turns the one before into flatMap, and a guard becomes withFilter.

  10. Question 10 of 14 Which calls does for s <- sizes; c <- colours yield s"$s-$c" become?

    Choose one answer.

    Show the answer: Question 10 of 14

    Answer: sizes.flatMap(s => colours.map(c => s"$s-$c"))

    The rewrite nests the generators: flatMap over sizes, map over colours inside it. The session printed the same four strings for both forms.

  11. Question 11 of 14 Why does the same for-comprehension syntax work on a List, an Option and a Future?

    Choose one answer.

    Show the answer: Question 11 of 14

    Answer: It is rewritten into map, flatMap and withFilter calls, and each of these types has those methods

    A for-comprehension is syntax for calls, so the type decides what they do: for a List they loop, for an Option they stop at None, and for a Future they run once the results are ready.

  12. Question 12 of 14 What is the only value of the type Unit?

    Choose one answer.

    Show the answer: Question 12 of 14

    Answer: ()

    A method that returns Unit, such as println or log, still returns (). That makes Unit an ordinary type, which generic code can use like any other.

  13. Question 13 of 14 missing is a Money set to null, and a is Money(450). What does missing == a give in Scala?

    Choose one answer.

    Show the answer: Question 13 of 14

    Answer: false

    The API documentation defines x == that as if (x eq null) that eq null else x.equals(that), so == never throws on null. The session printed missing == a: false.

  14. Question 14 of 14 System.getProperty returns null for a property that is not set. How do you handle that in Scala?

    Choose one answer.

    Show the answer: Question 14 of 14

    Answer: Wrap the result in Option(...) where it enters your code, then use getOrElse or a match

    Java APIs signal a missing value with null. Turn it into an Option where it enters Scala, as Option(home) did, and supply the default with getOrElse.

Interview questions

Fresher · What is the difference between == and eq in Scala, and how does it compare with Java and Python?

In Scala, == compares values: for objects it calls equals, and it handles null safely, because x == that is defined as if (x eq null) that eq null else x.equals(that). eq compares references: it is true only when both names point to the same object. So two case class values with the same fields are == but not eq. Java is the other way round: == on objects compares references and .equals compares values, which is why string comparisons with == are a classic Java bug. Python matches Scala: == compares values and is compares identity. In everyday Scala you use == almost always; eq is for the rare code where identity itself matters, such as a cache keyed on one particular object.

Key takeaways

  • The same program has the same parts in Java, Scala and Python, and here all three printed the same order total.
  • From Java: records become case classes, static members live in an object, switch becomes a match with a value, Optional becomes Option, and Scala has no checked exceptions.
  • From Python: frozen data classes become case classes, comprehensions become for-comprehensions, None becomes the None of an Option that the type declares, and the compiler checks what duck typing leaves to run time.
  • A for-comprehension is withFilter, map and flatMap calls, so it works on Option and other types as well as on collections.
  • == compares values and eq references, Unit is a value, Option(...) guards against null from Java, List never changes in place, and an Int wraps around at 2,147,483,647.

Terms in this lesson

case class
A class for immutable data. The compiler adds equals and hashCode that compare the fields, a readable toString, a copy method and support for pattern matching.
for-comprehension
A for expression with yield. The compiler rewrites it into map, flatMap and withFilter calls, so it works for lists, Option, Either and any other type with those methods.
object
A singleton: a class with exactly one instance, created when it is first used. The members of an object play the role of Java's static members.
Option
A container that holds either one value, Some(value), or nothing, None. Scala code uses it instead of null for a value that may be missing.
reference equality
Two names that point to the same object in memory. In Scala, a eq b tests it, while a == b calls equals and compares values.
Unit
The type of expressions that run for their effect, such as println. Its only value is written (), and it plays the role of void in Java.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.