Inheritance: Abstract, Sealed, Open & Final
|
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. |
This page is written against Scala 3.10 (3.10.0, 2026-10-12) and every example was run with Scala CLI and Scala 3.10.0
on JDK 26 on 2026-10-15 (the examples were not re-run on 3.9 or 3.3). open and the feature warning for ad-hoc
extension were added in Scala 3.0, so everything here is available on the 3.9 LTS and 3.3 LTS lines as well; only the wording
of some diagnostics depends on the compiler version. A Scala class decides how it may be extended with a small set of
modifiers: abstract (cannot be instantiated, may leave members undefined), final (cannot be extended), sealed
(may be extended only in its own file), open (explicitly designed to be extended), and a default of "closed in spirit":
a plain class that can be extended but only with a warning from another file. This page covers those modifiers and the
rules for override. Traits and mixin composition are in Traits & Mixins,
the sum types built with sealed are in Algebraic Data Types & GADTs,
and the visibility modifiers (private, protected) in Annotations & Visibility.
The examples use the trailhead scenario of this section: a toolkit that plans hikes on a network of mountain trails.
Abstract classes
An abstract class cannot be instantiated and may declare members without a body. Subclasses supply the missing
parts. A member with no body is abstract; the abstract keyword is required on the class but not on the members,
unlike Java.
//> using scala 3.10.0
abstract class Trailhead(val name: String, val elevationM: Int):
def difficulty: String // abstract member
def label: String = s"$name ($elevationM m, $difficulty)" // concrete member
override def toString: String = label
class Meadow(name: String) extends Trailhead(name, 1200):
def difficulty = "easy"
class Summit(name: String, elevationM: Int) extends Trailhead(name, elevationM):
def difficulty = if elevationM > 3000 then "hard" else "moderate"
override def label: String = "^ " + super.label
@main def i1(): Unit =
val all: List[Trailhead] = List(Meadow("Prado"), Summit("Aneto", 3404), Summit("Tobacor", 2779))
all.foreach(println)
Prado (1200 m, easy)
^ Aneto (3404 m, hard)
^ Tobacor (2779 m, moderate)
Source: Scala 3 Book: Abstract classes
What the example shows:
-
extends Trailhead(name, 1200)passes arguments to the parent constructor. A class has exactly one parent class, and its constructor arguments are given in theextendsclause. Further parents must be traits. -
A concrete subclass must define every abstract member.
MeadowandSummitimplementdifficultywithoutoverride, because implementing an abstract member does not need the keyword (it is allowed, and some teams write it for clarity). -
super.labelcalls the parent’s version, here from a method that replaces it. The rules for whatsupermeans once traits are mixed in are in Traits & Mixins. -
An abstract member can be a
def, avalor a type. An abstractvalcan be implemented by aval. Adefcan be implemented by adef, avalor alazy val(see the initialisation-order section for why this matters). -
An abstract class may have constructor parameters; a trait may as well since Scala 3 (trait parameters), so an abstract class is needed less often than in Java. Prefer a trait when no constructor argument or
finalinitialisation is required, and an abstract class when it is, or when Java code needs to extend it as a class.
Missing members are caught at compile time, and an abstract class cannot be instantiated. These are the real messages:
//> using scala 3.10.0
abstract class Trailhead(val name: String):
def difficulty: String
class Bad1 extends Trailhead("x")
[error] ./i2c.scala:6:7
[error] class Bad1 needs to be abstract, since def difficulty: String in class Trailhead is not defined
[error] class Bad1 extends Trailhead("x")
[error] ^^^^
Error compiling project (Scala 3.10.0, JVM (26))
Source: Scala 3 Book: Abstract classes
//> using scala 3.10.0
abstract class Trailhead(val name: String):
def difficulty: String
final def id: String = name.toLowerCase
def label: String = name
class Bad1 extends Trailhead("x")
@main def i2(): Unit =
val t = new Trailhead("q")
[error] ./i2.scala:11:15
[error] Trailhead is abstract; it cannot be instantiated
[error] val t = new Trailhead("q")
[error] ^^^^^^^^^
Error compiling project (Scala 3.10.0, JVM (26))
Source: Scala 3 Book: Abstract classes
(The compiler stops after the first phase that reports errors, so the missing difficulty of Bad1 appears only once
the instantiation error is fixed.) Without new, Trailhead("q") fails differently, with "Expected a term, but found a
type": a creator application cannot create an abstract class (see
Objects & Companions).
override and final
A member that replaces an inherited concrete member needs the override modifier; the compiler refuses silent
replacement. A member marked final cannot be replaced at all, and a final class cannot be extended.
//> using scala 3.10.0
abstract class Trailhead(val name: String):
def difficulty: String
final def id: String = name.toLowerCase
def label: String = name
class Bad2 extends Trailhead("y"):
def difficulty = "easy"
def label: String = "again"
def id: String = "z"
override def name = "n"
[error] ./i2b.scala:10:7
[error] error overriding method label in class Trailhead of type => String;
[error] method label of type => String needs `override` modifier
[error] def label: String = "again"
[error] ^
[error] ./i2b.scala:11:7
[error] error overriding method id in class Trailhead of type => String;
[error] method id of type => String cannot override final member method id in class Trailhead
[error] def id: String = "z"
[error] ^
[error] ./i2b.scala:12:16
[error] error overriding value name in class Trailhead of type String;
[error] method name of type => String needs to be a stable, immutable value
[error] override def name = "n"
[error] ^
Error compiling project (Scala 3.10.0, JVM (26))
Source: Scala 3 Book: Abstract classes
The three errors, in order:
-
A missing
override.labelis concrete inTrailhead, so replacing it needsoverride. The rule protects against accidental overrides when the parent later gains a method of the same name, and the keyword also makes the compiler reject anoverridethat overrides nothing (a typo in the name). -
A
finalmember.idisfinal, so the subclass cannot replace it. Usefinalfor members whose behaviour the class depends on, or that implement an invariant. Aprivatemember is never overridden, only hidden. -
A
valcan be overridden only by aval. Adefcannot replace aval, because avalis a stable value and adefis not; the other direction (avalreplacing adef) is fine.
The rules in summary:
| Modifier | Effect |
|---|---|
|
Cannot be instantiated; may have undefined members. |
|
Required to replace a concrete member; optional when implementing an abstract one. |
|
Cannot be overridden. |
|
Cannot be extended: |
|
Direct subtypes may be defined only in the same file; enables exhaustivity checking. |
|
The class is designed to be extended anywhere; extending it never warns. |
|
Visible in subclasses (and, unlike Java, not in the same package). |
Source: Scala 3 Reference: Open Classes
Java and Kotlin differ here: a Java method can be overridden unless final and a class can be extended unless final
or sealed; Kotlin classes and members are final by default and need open. Scala sits in between: everything is
open to extension unless final, but extension of an unmarked class from another file gets a warning, described below.
See Inheritance and Polymorphism in Java.
sealed
A sealed class or trait can be extended only by classes and objects in the same source file. The compiler therefore
knows all direct subtypes and can check a match for exhaustivity.
//> using scala 3.10.0
sealed trait Trail
final case class Loop(km: Double) extends Trail
final case class OneWay(km: Double) extends Trail
case object Closed extends Trail
def km(t: Trail): Double = t match
case Loop(k) => k
case OneWay(k) => k
@main def i3(): Unit = println(km(Closed))
[warn] ./i3.scala:8:28
[warn] match may not be exhaustive.
[warn]
[warn] It would fail on pattern case: Closed
[warn] def km(t: Trail): Double = t match
[warn] ^
Exception in thread "main" scala.MatchError: Closed (of class Closed$)
at i3$package$.km(i3.scala:10)
at i3$package$.i3(i3.scala:12)
at i3.main(i3.scala:12)
The warning is the benefit of sealed: the missing Closed case is reported at compile time, before the program
throws a MatchError at run time. A new case added later makes every non-exhaustive match in the code base warn. A
final modifier on the subclasses (as above) closes the hierarchy at the leaves. Attempting to extend a sealed type from
another file is an error. With sealed trait Shape in one file and a case class in another:
[error] ./i4b.scala:1:12
[error] Cannot extend sealed trait Shape in a different source file
[error] case class Other(s: String) extends Shape
[error] ^
Error compiling project (Scala 3.10.0, JVM (26))
Notes on sealed:
-
A
sealedclass can itself beabstract(sealed abstract class), in which case it cannot be instantiated; asealed traitis already abstract. -
Subclasses of a sealed type are not themselves sealed unless declared so. A subclass that is neither
final,sealednorcaseleaves a hole in exhaustivity reasoning, so make leavesfinal(orcase, which Scala 3 does not make final). -
sealedandopenare mutually exclusive:sealed open class Afails with "illegal combination of modifiers:sealedandopenfor: class A". -
Java 17 sealed classes with
permitsare the nearest Java equivalent (see Records and Sealed Classes in Java). Kotlin’ssealed classis the same idea (see Sealed Classes and Enums in Kotlin).
For the enum form of a sealed hierarchy, see Enums; case classes are in
Case Classes.
open and ad-hoc extension
A class that is not final, not sealed and not abstract can be extended anywhere, but doing so from a different
file than the parent is a feature warning in Scala 3 unless the parent is declared open. The message names the
reason: the author of a plain class has not promised that its internal calling patterns will stay stable. The reference says the warning is produced "only under -source future" and "by default from Scala 3.4 on". Checked
here on 3.10.0, it was still not produced by default: it appeared only with -feature and -source:future (or
future-migration), and not with -feature alone at the default source level. Treat the reference sentence about 3.4 as not yet
true of 3.10.0, and turn the warning on explicitly if you want it. The layouts below were compiled as files in package p
with scala run . -O -feature -O -source:future.
// Hut.scala
package p
class Hut(val name: String):
def describe: String = s"Hut $name"
// Cabin.scala
package p
class Cabin(n: String) extends Hut(n)
[warn] ./Cabin.scala:2:32
[warn] Unless class Hut is declared 'open', its extension in a separate file should be enabled
[warn] by adding the import clause 'import scala.language.adhocExtensions'
[warn] or by setting the compiler option -language:adhocExtensions.
[warn] See the Scala docs for value scala.language.adhocExtensions for a discussion
[warn] why the feature should be explicitly enabled.
[warn] class Cabin(n: String) extends Hut(n)
[warn] ^^^^^^
Hut A
Source: Scala 3 Reference: Open Classes
There are two ways to silence it. The author of the parent marks it open, which says "extension is part of the
contract", or the author of the child opts in with the language import:
// Hut.scala
package p
open class Hut(val name: String):
def describe: String = s"Hut $name"
// or, in Cabin.scala, leaving Hut unchanged:
package p
import scala.language.adhocExtensions
class Cabin(n: String) extends Hut(n)
Both variants compiled with no warning (Hut A printed). The options have different meaning:
-
open classis a promise by the author of the parent: there is a documented extension contract describing which methods are hooks, which calls the class makes on itself and what a subclass may rely on. -
import scala.language.adhocExtensionsis a decision by the extender, who accepts that the parent’s author made no promise (typical for a test double or a temporary patch to a library class) and that a later release may break the extension. -
openis a soft keyword: an ordinary identifier except in modifier position. An open class cannot befinalorsealed, and a trait or an abstract class is always open, so the modifier is redundant on them. -
openandsealedcombine across parents. Anopensubclass of asealedparent is allowed (checked:open class Pass extends Thundersealed abstract class Th, withPassextended from another file, compiled), while extendingThitself from another file is the error "Cannot extend sealed class Th in a different source file". -
Extension in the same file never warns, because the author of the parent controls that file. Using it, a class that is neither open nor abstract can still have a few subclasses next to it, which is how
sealedhierarchies are written.
Source: Scala 3 Reference: Open Classes
The reference puts the relationship this way: a class that is neither abstract nor open is similar to a sealed class, but
extending it from another file is permitted with the adhocExtensions feature (and a warning without it), whereas for a
sealed class it is an error. A sealed hierarchy can mix all the modifiers:
//> using scala 3.10.0
sealed abstract class Trailhead(val name: String)
final class Meadow(name: String) extends Trailhead(name)
sealed class Summit(name: String) extends Trailhead(name)
final class Aneto extends Summit("Aneto")
open class Pass(name: String) extends Trailhead(name)
@main def i8(): Unit =
val t: Trailhead = Aneto()
println(t match
case _: Meadow => "meadow"
case _: Summit => "summit"
case _: Pass => "pass")
summit
The hierarchy is shown below. Pass is open below a sealed parent: other files may extend Pass, while Meadow and Aneto end the chain and
only this file may add direct subclasses of Trailhead or Summit.
Planning for extension
The Scala 3 Book recommends deciding on purpose how each class may be extended:
-
Meant to be extended: mark it
open(or make itabstractor a trait), document which methods are hooks and which are called by which, and keep that contract stable. -
Not meant to be extended: mark it
final. This is the safest default for value-like classes, and case classes are better asfinal case class. -
Closed set of alternatives: use
sealed(or anenum), put every subtype in the file, and let the compiler checkmatchexpressions. -
No decision: leave the modifier off. That is the "might be extended ad hoc by someone else, on their own risk" state that the feature warning pushes you to make explicit.
Two consequences follow. A method that a subclass overrides is part of the public contract of the class, so an open
class should make helper methods private or final. And a public member of a trait or abstract class that is never
meant to be changed should be final.
Initialization order and early initializers
A superclass constructor runs before the subclass body, so a parent that reads an abstract val sees its default value.
This is the classic Scala initialization trap, and it is the same in Java for overridden methods:
//> using scala 3.10.0
abstract class Parent:
val size: Int
println(s"Parent init: size = $size")
class Child extends Parent:
val size = 10
println(s"Child init: size = $size")
abstract class Parent2:
def size: Int
println(s"Parent2 init")
class Child2 extends Parent2:
lazy val size = 10
trait HasSize(val size: Int):
println(s"HasSize init: size = $size")
class Child3 extends HasSize(10)
@main def i7(): Unit =
Child()
Child2()
Child3()
Parent init: size = 0
Child init: size = 10
Parent2 init
HasSize init: size = 10
-
Parentprintedsize = 0, not 10: the parent ran first, so the child’svalwas not yet assigned. Reading an abstractvalfrom a parent constructor is almost always a bug. -
Abstract
defpluslazy valin the child (Child2) avoids it, becauselazy valis computed on first use. -
A trait or class parameter (
HasSize(10)) is the Scala 3 answer: the argument is available while the parent initialises, with no ordering problem. See Traits & Mixins for trait parameters.
Scala 2 had early initializers (class Early extends { val x = 1 } with Base) for this, and Scala 3 dropped them. The
compiler says what to do instead:
[error] ./i6.scala:7:21
[error] `extends` must be followed by at least one parent
[error] class Early extends { val x = 1 } with Base
[error] ^
[error] ./i6.scala:7:35
[error] Early definitions are not supported; use trait parameters instead
[error] class Early extends { val x = 1 } with Base
[error] ^^^^
[error] ./i6.scala:7:40
[error] end of toplevel definition expected but identifier found
[error] class Early extends { val x = 1 } with Base
[error] ^^^^
Error compiling project (Scala 3.10.0, JVM (26))
The Scala 3 compiler also has a safe-initialization check (-Wsafe-init) that finds such reads; it is described in
Traits & Mixins and in the
Scala 3 Reference: Safe Initialization.
Pitfalls
-
Forgetting
overrideis an error, and so is a typo in anoverridename.override def hikeefor a parent methodhikefails with "method hikee overrides nothing"; that is the benefit of the keyword. -
Reading an abstract
valin a parent constructor sees0/null/false. Use a trait or class parameter, alazy valor adef. -
A
sealedhierarchy split across files is an error, and a package split of the same source is no help: all direct subtypes must live in the one file. -
A non-
finalleaf class weakens exhaustivity. Pattern matching onTrailheadcannot be fully exhaustive if an unmarked subclass may be extended elsewhere; mark leavesfinalorsealed. -
openis not a visibility modifier. It does not say where a class can be used, only that it may be extended. -
Case classes inheriting from other case classes are forbidden:
case class B(...) extends A(x)withAa case class fails with "case-to-case inheritance is prohibited"; use a trait or an abstract class as the parent. See Case Classes. -
The warning needs
-feature. Without the flag, the compiler may only print a summary line asking you to re-run with it, or nothing; treat it as an error in CI by enabling-feature -Werror.
Where the books are outdated
The books consulted for this section (see the section bibliography) were written for Scala 2 or for the first Scala 3 releases. For inheritance, treat these points as superseded by the pages cited here:
-
Early initializers (
extends { val x = ... } with T) are Scala 2 and are errors in Scala 3; use trait parameters. -
opendoes not exist in the Scala 2 books, which treat a plain class as freely extensible. -
sealed,final,abstractandoverrideare unchanged since Scala 2, and the explanations still hold; thethenand braceless syntax of the examples is what differs. -
Case-class inheritance and enumerations are handled differently: Scala 3 prefers
enumandfinal case classhierarchies oversealed abstract classwithEnumeration.
The run results above are from 2026-10-15 on Scala 3.10.0 and were not re-run on the 3.9 LTS or 3.3 LTS releases.