Scala for Java, Python & JavaScript Developers

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

If you already write Java, Python or JavaScript, you know most of the ideas Scala uses: classes, lambdas, collections, generics, async code. What changes is the spelling and the defaults. This page is a translation table with working examples, one section per area, followed by the places where assuming "it works like my language" gives a compile error or, worse, a different answer. The baseline is Scala 3.10.0 (released 2026-10-12, JDK 17 or newer) Every example was executed with Scala CLI on Scala 3.10.0 and JDK 26 on 2026-10-15, and the output shown is what the run printed. The syntax used dates from Scala 3.0, so none of it is tied to a 3.10-only feature, and the 3.9 LTS line accepts the same programs, but only 3.10.0 was run here.

The examples use the trailhead scenario of this section, a toolkit that plans hikes on a network of mountain trails. The official Scala 3 Book has one page per source language, and this page follows their structure but writes its own examples: Scala for Java Developers, Scala for Python Developers and Scala for JavaScript Developers. The language sections of this site cover the other side of each comparison: Java, Python, JavaScript and Kotlin (the closest relative if you come from the JVM).

The shared picture

At the highest level the three comparisons agree on what Scala is. It is statically typed like Java, but type inference makes it read almost like Python or JavaScript. It compiles to JVM class files like Java, so every Java library is available, and the same source can also target JavaScript (Scala.js) or native code (Scala Native). Everything is an object, and operators such as + are methods. Almost every construct is an expression that produces a value, and immutability is the default style.

Idea Java Python JavaScript Scala 3

Immutable binding

final int n = 1;

(a convention only)

const n = 1;

val n = 1

Mutable binding

int n = 1;

n = 1

let n = 1;

var n = 1

Data carrier

record Trail(...) (16)

@dataclass(frozen=True)

object literal or class

case class Trail(...)

Interface or mixin

interface with default methods

Protocol or abstract base class

(none; mixin functions)

trait

Enumeration

enum

enum.Enum

frozen object

enum

Absent value

null, Optional

None

null, undefined

Option (and null for Java interop)

Conditional value

a ? b : c

b if a else c

a ? b : c

if a then b else c

Multi-way branch

switch (21: pattern matching)

match (3.10)

switch

match, with patterns and guards

Async result

CompletableFuture

asyncio coroutine

Promise

Future and Promise

Entry point

public static void main

if __name__ == "__main__":

top-level code or module

@main def

The rest of the page expands each row.

Variables, types and naming

val is a binding that cannot be reassigned (Java final, JavaScript const) and var can be reassigned (JavaScript let). The type is inferred from the right-hand side, and you add it only where the compiler needs it or you want it documented: on public method results and parameters. Naming follows Java and JavaScript (lengthKm, not Python’s length_km), classes are UpperCamelCase, and there are no semicolons or new for most classes.

//> using scala 3.10.0

enum Difficulty:
  case Easy, Moderate, Hard

case class Trail(name: String, lengthKm: Double, ascentM: Int, difficulty: Difficulty)

@main def a1(): Unit =
  val name = "Pico Alto"          // like final String / const
  var lengthKm = 12.5             // like double / let (reassignable)
  lengthKm += 1.5                 // no ++ and -- in Scala
  val trail = Trail(name, lengthKm, 940, Difficulty.Hard)  // no `new` needed
  val renamed = trail.copy(name = "Pico Bajo")
  println(trail)
  println(renamed)
  println(f"${trail.name}%s: ${trail.lengthKm}%.1f km, ${trail.ascentM} m")
  assert(trail == Trail("Pico Alto", 14.0, 940, Difficulty.Hard)) // == compares fields
  assert(trail != renamed)
Trail(Pico Alto,14.0,940,Hard)
Trail(Pico Bajo,14.0,940,Hard)
Pico Alto: 14,0 km, 940 m

The last line shows a trap for anyone used to JavaScript template strings or Python f-strings. Scala has three interpolators: s"..." (any expression with ${...}), f"..." (printf-style formats, checked at compile time) and raw"...". The f interpolator formats through Java’s String.format, so it uses the JVM’s default locale, and the machine that produced this output uses a decimal comma. Pass an explicit Locale with "%.1f".formatLocal(Locale.ROOT, x) when the output must not depend on the machine. Strings are always double-quoted: 'a' is a Char, and multi-line text uses triple quotes plus stripMargin (the equivalent of a Java text block or a Python triple-quoted string). See Values, Variables & Literals and Strings & Interpolation.

Types are the same family as in Java, with two differences. There is no split between primitives and wrappers: Int, Double, Boolean and the others are ordinary classes that the compiler represents as JVM primitives when it can, so 3.max(9) is valid and Int can be a generic argument without boxing syntax. Generics use square brackets, List[Int], because < and > are ordinary operator names. Numeric types do not have ++ or --, and an Int is 32-bit exactly as in Java, while the JavaScript number corresponds to Double and the JavaScript bigint to BigInt.

Classes, records and traits

A Scala class declares its fields in the constructor: class Hut(val name: String, val beds: Int) is a Java class with two fields, getters and a constructor. val makes a read-only field, var a read-write one, and no keyword makes a plain constructor parameter. A case class adds equals, hashCode, toString, copy and a pattern-matching extractor, and it is the equivalent of a Java record or a frozen Python dataclass (the first example already used copy). Case classes and enums cover both in depth.

Scala has no interface. A trait is an interface whose members can have implementations and state, and a class can extend several traits. The compiler linearizes them, which is why Java’s diamond problem becomes a defined order instead of a compile error. A class lists its parents after extends, separated by commas.

//> using scala 3.10.0

trait Describable:
  def describe: String

trait Timed:
  def minutes: Int
  def label: String = s"$minutes min"

class Hut(val name: String, val beds: Int) extends Describable, Timed:
  def minutes = 45
  def describe = s"$name ($beds beds), $label"

@main def a2(): Unit =
  val h = Hut("Refugio", 30)
  println(h.describe)
  // an anonymous mix-in at the call site, like a Java anonymous class
  val extra = new Describable with Timed:
    def minutes = 5
    def describe = s"Water point, $label"
  println(extra.describe)
Refugio (30 beds), 45 min
Water point, 5 min

Three defaults differ from Java. A member with no modifier is public, not package-private. A class meant for extension is declared open class, which documents the "design for inheritance or forbid it" rule (reference: open classes). The compiler does not enforce it yet: on 3.10.0, extending a class without open from another file compiled with no message, and only -source:future adds a feature warning (silenced by import scala.language.adhocExtensions). And Scala’s protected is narrower than Java’s, because it does not grant access to other classes in the same package. See Traits & Mixins and Annotations & Visibility.

A Python developer will notice that the indentation after the colon is the block. Scala 3 allows either braces or significant indentation, and you can mix them across files; the section on optional braces has the exact rules and the compiler flags that rewrite code from one style to the other.

Expressions everywhere

The most useful mental shift is that Scala has almost no statements. if, match, try and for ... yield each produce a value, so you assign their result instead of declaring a variable first and mutating it. if takes then instead of parentheses, match replaces switch, and for loops use <- generators.

//> using scala 3.10.0

enum Difficulty:
  case Easy, Moderate, Hard
  case Alpine(grade: Int)

def parse(s: String): Option[Int] =
  try Some(s.toInt) catch case _: NumberFormatException => None

@main def a3(): Unit =
  val grade = 3
  // if, match and try are expressions: each one produces a value
  val label = if grade > 2 then "steep" else "gentle"
  val d = grade match
    case 0 => Difficulty.Easy
    case 1 | 2 => Difficulty.Moderate
    case g if g > 5 => Difficulty.Hard
    case g => Difficulty.Alpine(g)
  val n = try "12x".toInt catch case _: NumberFormatException => -1
  println(s"$label $d $n")
  // for ... yield builds a collection; for ... do just loops
  val squares = for i <- 1 to 4 yield i * i
  println(squares)
  for i <- 1 to 3; j <- 'a' to 'b' do print(s"$i$j ")
  println()
  println(parse("42"))
  println(parse("x"))
  println(parse("42").map(_ + 1).getOrElse(0))
  assert(label == "steep" && n == -1)
steep Alpine(3) -1
Vector(1, 4, 9, 16)
1a 1b 2a 2b 3a 3b
Some(42)
None
43

Compared with the other languages:

  • Java. switch falls through unless you write break, and the older form only matches constants. A Scala match never falls through, matches types, tuples and case-class shapes, takes guards, and on an enum or sealed type the compiler warns about missing cases. Java 21 pattern matching closes much of that gap (see Java pattern matching). The ternary operator does not exist, because if a then b else c is already an expression.

  • Python. match appeared in Python 3.10 and is a statement. A comprehension [i * 10 for i in xs if i % 2 == 0] is for i <- xs if i % 2 == 0 yield i * 10, and a Python generator expression corresponds to a LazyList.

  • JavaScript. === and !== do not exist: == and != are the only comparisons, and they never coerce between unrelated types.

Pattern Matching and For Comprehensions go much further.

Collections

Scala’s default collections are immutable and persistent: List, Vector, Set and Map never change, and operations such as :+, + or updated return a new collection that shares structure with the old one. Java’s List.of also gives an immutable list, but it has no such operations. The mutable classes live in scala.collection.mutable. The Python list and the JavaScript array are mutable, so the nearest match is ArrayBuffer or an Array, not List.

//> using scala 3.10.0
import scala.collection.mutable.ArrayBuffer

@main def a4(): Unit =
  val lengths = List(12.5, 8.0, 21.3)       // immutable, like List.of in Java
  val more = lengths :+ 5.5                  // a NEW list; `lengths` is unchanged
  println(lengths)
  println(more)
  val byName = Map("Pico" -> 940, "Lago" -> 120)   // immutable Map
  println(byName("Pico"))                    // parentheses, not [] or .get
  println(byName.get("Cumbre"))              // Option, never null
  val buf = ArrayBuffer(1, 2, 3)             // the mutable list (Python list, JS array)
  buf(0) = 10                                // update with parentheses
  buf += 4
  println(buf)
  val (short, long) = lengths.partition(_ < 15)
  println(s"$short $long")
  println(lengths.map(_ * 2).filter(_ > 20).sum)
  assert(lengths.size == 3 && more.size == 4)
List(12.5, 8.0, 21.3)
List(12.5, 8.0, 21.3, 5.5)
940
None
ArrayBuffer(10, 2, 3, 4)
List(12.5, 8.0) List(21.3)
67.6

Equivalents to keep at hand:

Java Python JavaScript Scala

List.of(1, 2)

[1, 2]

[1, 2]

List(1, 2) or Vector(1, 2)

new ArrayList<>()

xs = []

xs = []

ArrayBuffer[Int]()

xs.get(0)

xs[0]

xs[0]

xs(0)

Map.of("a", 1)

{"a": 1}

new Map(...)

Map("a" -> 1)

Set.of(1, 2)

{1, 2}

new Set([1, 2])

Set(1, 2)

stream.map(f).toList()

[f(x) for x in xs]

xs.map(f)

xs.map(f)

stream.filter(p)

filter(p, xs)

xs.filter(p)

xs.filter(p)

Pair from a library

(1, "a")

[1, "a"]

(1, "a"), accessed as t(0)

Two differences deserve emphasis. First, the methods are defined on the collection itself (xs.map(...)), eagerly, without a .stream() step as in Java; use .view or LazyList for laziness. Second, a Scala Array[Int] is the real JVM int[], so it interoperates with Java arrays, but it is rarely the right default. Java collections are converted with scala.jdk.CollectionConverters (Using Java from Scala), and the whole library is covered from Collections Overview. The Java side is in Collections Framework and Streams & Collectors.

Functions and lambdas

A method is declared with def, and a lambda is written (x: Int) => x * 2. Most of the time you pass a lambda where the parameter type is already known, so the type is dropped (xs.map(x => x * 2)), and for one-use parameters the placeholder form xs.map(_ * 2) is idiomatic. A method used where a function is expected is converted automatically (eta-expansion), so you can write xs.map(parse) where Java would write Main::parse. Unlike Java, a lambda is not restricted to effectively-final captured variables, and unlike JavaScript, this is not rebound inside it. Lambdas and method references shows the Java equivalents.

Parameters may have default values and be passed by name (plan(trail, speed = 4.0)), a method can have several parameter lists, and a block can be passed as the last argument, which is how Scala libraries build their own control structures. A Python developer can think of these as the function-call features they already use, and a Java developer gets the equivalent of overloaded methods and builders for free. The details are in Methods & Parameters.

Null, absence and errors

Idiomatic Scala does not use null. A value that may be missing has the type Option[A], which is either Some(a) or None; a computation that may fail has the type Either[E, A] or Try[A]. The compiler makes you handle the empty case before using the value, so the Java NullPointerException, the Python AttributeError: 'NoneType' and the JavaScript Cannot read properties of undefined become type errors or explicit defaults.

//> using scala 3.10.0
import java.util.Optional

@main def n1(): Unit =
  val fromJava: String | Null = System.getProperty("trailhead.missing")   // a Java method may return null
  val safe = Option(fromJava).getOrElse("default")                       // wrap at the boundary
  println(safe)
  val opt: Optional[String] = Optional.empty()
  println(opt.isPresent)
  val hut: Option[String] = None
  println(hut.fold("no hut")(h => s"hut: $h"))
default
false
no hut

null still exists because Java returns it, and Option(x) turns a possibly-null reference into an Option at the boundary. By default val s: String = null compiles. The compiler flag -Yexplicit-nulls (listed under the experimental features of the reference) removes Null from the reference types, so the same line becomes an error, as this run shows:

[error] ./n2.scala:4:19
[error] Found:    Null
[error] Required: String
[error] Note that implicit conversions were not tried because the result of an implicit conversion
[error] must be more specific than String
[error]   val s: String = null
[error]                   ^^^^

Exceptions also differ. Scala has no checked exceptions: a method never declares throws, and the compiler does not force a catch. The catch block is a pattern match (catch case e: IOException => ...), and the idiom for expected failures is to return Option, Either or Try rather than throw. When Java code must see the exception type, annotate the method with @throws. See Option & Null Safety and Exceptions, boundary & Early Exit.

Concurrency and asynchronous code

The standard asynchronous type is scala.concurrent.Future, with Promise as its write side. A Future starts running as soon as it is created, on an ExecutionContext (a thread pool). Transformations use map, flatMap and recover, and a for comprehension chains them the way await chains async calls. There are no async/await keywords in the language, so the for comprehension fills that role.

//> using scala 3.10.0
import scala.concurrent.{Await, Future}
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration.*

def forecast(hut: String): Future[Int] = Future { hut.length * 3 }   // runs on a pool thread

@main def a5(): Unit =
  val f = for
    a <- forecast("Refugio")
    b <- forecast("Pico")
  yield a + b                          // like thenCompose / await / .then
  val recovered = Future.failed[Int](RuntimeException("no signal")).recover { case _ => -1 }
  println(Await.result(f, 2.seconds))
  println(Await.result(recovered, 2.seconds))
33
-1
Need Java Python JavaScript Scala

Async value

CompletableFuture<T>

coroutine, asyncio.Future

Promise<T>

Future[T]

Chain steps

thenCompose, thenApply

await

.then, await

flatMap, map, for

Recover

exceptionally

try/except

.catch

recover, recoverWith

Where it runs

common pool or an Executor

event loop

event loop

an implicit ExecutionContext

Blocking

get()

run_until_complete

(not possible)

Await.result (avoid in real code)

Python and JavaScript run on one event-loop thread, so shared mutable state is safe between awaits. A Scala Future runs on real threads, so, as in Java, mutable state shared between futures needs synchronization or, better, an immutable design. Threads on JDK 21 or newer, including virtual threads (Virtual Threads), are available to Scala code through the Java APIs. Parallel collections and the larger concurrency libraries are covered in Futures & Promises and Parallel Collections & Concurrency Libraries.

Tooling

You do not need a heavy toolchain to start. Scala CLI compiles and runs a single file (every example above was run as scala run file.scala), and sbt, Mill, Maven or Gradle handle multi-module projects. Packages come from Maven Central, the same repository Java uses, so your existing Maven knowledge transfers.

Task Java Python JavaScript Scala

Run a single file

java File.java, jshell

python file.py

node file.js

scala run file.scala

Interactive shell

jshell

python

node

scala repl

Build and dependencies

Maven, Gradle

pip, venv, uv

npm, pnpm

sbt, Mill, Scala CLI, Maven, Gradle

Tests

JUnit

pytest

Jest

MUnit, ScalaTest, ScalaCheck

Formatter

google-java-format

black, ruff format

Prettier

Scalafmt

IDE

IntelliJ IDEA, VS Code

PyCharm, VS Code

VS Code, WebStorm

IntelliJ IDEA with the Scala plugin, VS Code with Metals

See Installing Scala, Scala CLI and sbt & Build Tools. JavaScript developers can also target the browser and Node with Scala.js; the official Scala.js site has tutorials aimed at JavaScript programmers, and the Scala 3 Book page for JavaScript developers includes a short DOM sample.

Valid Java is not Scala

Scala looks familiar enough that it is tempting to type Java and hope. The following surprises cover most first-week errors. The compiler messages are real, from Scala 3.10.0.

Declarations and operators

A declaration is val name: Type = value, with the type after the name. Java-style int count = 0; is rejected:

[error] ./e1.scala:3:7
[error] end of statement expected but identifier found
[error]   int count = 0;
[error]       ^^^^^
[error] ./e1.scala:3:3
[error] Not found: int - did you mean Int?
[error]   int count = 0;
[error]   ^^^

Several habits fail in the same way:

[error] ./e2.scala:4:4
[error] end of statement expected but identifier found
[error]   i++
[error]    ^^
[error] ./e2.scala:6:11
[error] method apply in trait LinearSeqOps does not take type parameters
[error]   println(xs[0])
[error]           ^^^^^

and, for reassigning a val and calling a parameterless method with parentheses:

[error] ./e3.scala:4:3
[error] Reassignment to val n
[error]   n = 6
[error]   ^^^^^
[error] ./e3.scala:6:11
[error] method length in class List does not take parameters
[error]   println(xs.length())
[error]           ^^^^^^^^^

So: use i = 1` instead of `i+, index with xs(0) because indexing is an apply method call, and call methods declared without a parameter list, such as length, without parentheses. Scala 3 enforces this for methods defined in Scala; methods that come from Java may be called either way.

Equality, expressions and numbers

//> using scala 3.10.0
class Trail(val name: String)
@main def e4(): Unit =
  val a = Trail("Pico")
  val b = Trail("Pico")
  println(a == b)
  println(a eq b)
  println("Pico" == String("Pico"))
  println(List(1, 2) == Vector(1, 2))
  val x = if false then 1
  println(x)
  val big = 2147483647
  println(big + 1)
  println(7 / 2)
  println(-7 % 3)
  println(1 == 1L)
[warn] ./e4.scala:10:25
[warn] Discarded non-Unit value of type Int. Add `: Unit` to discard silently.
[warn]   val x = if false then 1
[warn]                         ^
false
false
true
true
()
-2147483648
3
-1
true

Reading the output line by line:

  • a == b is false because Trail is a plain class that does not override equals. In Scala == is equals (null-safe, so null == x does not throw), and eq is reference identity, the Java == on objects. That is why "Pico" == String("Pico") is true, whereas Java’s == would compare references. A case class generates a structural equals, and two collections of different types but equal contents (List(1, 2) == Vector(1, 2)) are equal too. Multiversal equality can forbid meaningless comparisons at compile time; see Multiversal Equality.

  • An if without else is an expression whose type is Unit, the value (), when the condition is false. The compiler warns that 1 was thrown away. Always give an else when you use the value.

  • Int arithmetic is exactly Java’s: overflow wraps (2147483647 + 1 is -2147483648), / truncates and % keeps the sign of the dividend. Scala does not widen to Double for you, and comparing 1 == 1L is true because the numeric values are equal.

The next program shows the remaining differences from Java: no throws clause, object instead of static, and an Int that is both a JVM int and an object.

//> using scala 3.10.0
import java.io.IOException
def read(): String = throw IOException("disk")       // no `throws` clause required
object Util:                                        // replaces static members
  val Max = 10
  def twice(i: Int) = i * 2
@main def e5(): Unit =
  val r = try read() catch case e: IOException => s"caught ${e.getMessage}"
  println(r)
  println(Util.twice(Util.Max))
  val i: Int = 3
  val o: Any = i                                   // boxing is hidden: Int is also an object
  println(i.toString + " " + i.max(9) + " " + o.isInstanceOf[Int])
  println(classOf[Int])
caught disk
20
3 9 true
int
  • No static. A singleton object holds what Java puts in static members, and a class and an object with the same name in the same file are companions that can see each other’s private members. Top-level def and val are also allowed, so a file does not need to be wrapped in a class, and its name does not have to match anything it defines.

  • No checked exceptions. read() throws an IOException with no declaration, and the caller catches it with a pattern.

  • No primitive/wrapper split. 3.max(9) works, Any accepts an Int without ceremony, and classOf[Int] is the JVM int class.

  • Operators are methods. 1 + 2 is 1.+(2), and any method with one argument can be used infix, which is how libraries build DSLs.

  • Wildcards are ?. A Java List<?> is written List[?], and a Java List<? extends T> is List[? <: T], or better a covariant type declared once with List[+A] on the definition (see Variance).

  • main is not special syntax. @main def run(): Unit is the entry point, and arguments can be typed parameters that the compiler parses for you.

Where the books and older material are outdated

  • The Scala 3 Book comparison for Java developers says that some Scala concepts have no equivalent in "Java 11" and compares case classes with "records in Java 14". Records are final in Java 16, sealed types in 17 and pattern matching for switch in 21, so those comparisons are now closer than the page suggests. The Java section here is the reference for the current Java side.

  • The JavaScript page shows a jQuery-based Scala.js sample. It illustrates syntax only; the current Scala.js documentation is the place to start a new project.

  • Scala 2-era books and tutorials teach braces, implicit parameters and classes, sealed trait plus case classes instead of enum, and import scala.collection._ instead of import scala.collection.*. Much of that is still accepted by Scala 3, some of it only with warnings or migration flags (see Incompatibilities & Dropped Features), but the idiomatic Scala 3 forms on this page are what new code should use.

  • Python comparisons written before Python 3.10 say that only Scala has pattern matching. Both languages have it now, though Scala’s match is an expression.

  • The comparison pages were written for the first Scala 3 releases and do not mention later additions such as named tuples; Release History & What’s New lists them.

  • The books used for this section are listed in the bibliography, with the editions they cover.