Scala Case Classes

Learn to create classes and case classes in Scala, comparing them with Python classes. This lesson provides hands-on exercises, troubleshooting, and guidance on when to use each.

Focus: create classes and case classes

Sponsored

If you've ever written a Python class and wished the compiler had your back when you typo'd an attribute name, or you've spent hours writing __repr__, __eq__, and __hash__ boilerplate, Scala's case classes are about to become your new best friend. In this lesson, you'll learn how to create both regular classes and the much more powerful case classes, and you'll see exactly how they differ from Python classes — so you can write idiomatic, robust Scala code from day one.

The problem this lesson solves

In Python, classes are flexible and dynamic. You can add attributes on the fly, and you often write a lot of boilerplate for common operations like printing, comparing, and hashing. For example, to make a simple Point class that you can print and compare, you'd write something like:

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __repr__(self):
        return f"Point({self.x}, {self.y})"

    def __eq__(self, other):
        if not isinstance(other, Point):
            return False
        return (self.x, self.y) == (other.x, other.y)

    def __hash__(self):
        return hash((self.x, self.y))

That's a lot of code for a simple data holder, and it's easy to make mistakes—like forgetting to implement __hash__ when you override __eq__. In Scala, you face a similar problem: you want to define a lightweight data structure, but a regular class requires you to manually define constructors, getters, equality, and more. Case classes eliminate that boilerplate and give you powerful pattern matching out of the box.

Core concept / mental model

Think of a regular Scala class as a Java-style class — you control everything: constructor, methods, fields, and behavior. A case class is a data carrier — it's designed to hold immutable data and provide structural equality, hashing, and a readable string representation automatically.

If you're coming from Python, here's a quick mapping:

  • Regular Scala class ≈ a Python class with explicit __init__, methods, and custom behavior.
  • Case class ≈ a Python @dataclass(frozen=True) — plus pattern matching superpowers.

In Scala, a case class automatically gives you:

  • A constructor with named parameters
  • Getters (accessors) for each parameter
  • equals and hashCode based on the constructor parameters
  • A toString that prints a readable representation
  • A copy method to create modified copies
  • Support for pattern matching (you'll see this later)

Why immutability matters

Case class parameters are val (immutable) by default. This aligns with functional programming principles: you share data without worrying about side effects. In Python, you're used to mutable objects, but in Scala, prefer immutability unless you have a strong reason not to.

How it works step by step

Let's break down creating both a regular class and a case class.

Step 1: Create a regular class

A regular class has a primary constructor that's part of the class signature. You can define parameters with val or var to make them fields. Here's a simple regular class:

class Person(val name: String, var age: Int) {
  def greet(): String = s"Hi, I'm $name and I'm $age years old."
}
  • val name makes name a read-only field (getter only)
  • var age makes age mutable (getter and setter)
  • Methods are defined inside the class body

Step 2: Create a case class

A case class is declared with case class. Let's create a simple one:

case class Point(x: Int, y: Int)

That's it! You get all the automatic features. You can instantiate it normally:

val p = Point(1, 2)
println(p)        // Point(1,2)
println(p.x)      // 1

Step 3: Use the copy method

One of the handiest methods is copy. It lets you create a new instance with some fields changed:

val p1 = Point(1, 2)
val p2 = p1.copy(y = 5)
println(p2) // Point(1,5)

Step 4: Pattern matching with case classes

Case classes work beautifully with pattern matching. Here's an example:

def describe(point: Point): String = point match {
  case Point(0, 0) => "origin"
  case Point(x, 0) => s"on x-axis at $x"
  case Point(0, y) => s"on y-axis at $y"
  case Point(x, y) => s"point at ($x, $y)"
}

This is a huge productivity boost compared to Python, where you'd need manual isinstance checks or if conditions.

Hands-on walkthrough

Let's put this into practice. We'll create a small domain model using both regular and case classes.

Example 1: Basic class and case class in a Scala script

Create a file person.scala and run it with scala-cli or scala (if you have Scala 2/3 installed).

// Regular class
class Person(val name: String, var age: Int) {
  def birthday(): Unit = age += 1
  override def toString: String = s"Person(name=$name, age=$age)"
}

// Case class
case class Address(street: String, city: String, zip: String)

// Use them
val alice = new Person("Alice", 30)
alice.birthday()
println(alice) // Person(name=Alice, age=31)

val home = Address("123 Main St", "Springfield", "12345")
println(home)  // Address(123 Main St,Springfield,12345)

// Case class equality
val home2 = Address("123 Main St", "Springfield", "12345")
println(home == home2) // true

Expected output:

Person(name=Alice, age=31)
Address(123 Main St,Springfield,12345)
true

Example 2: Pattern matching a case class hierarchy

Let's model shapes as a sealed trait with case classes for each shape.

sealed trait Shape
case class Circle(radius: Double) extends Shape
case class Rectangle(width: Double, height: Double) extends Shape
case class Square(side: Double) extends Shape

def area(shape: Shape): Double = shape match {
  case Circle(r) => math.Pi * r * r
  case Rectangle(w, h) => w * h
  case Square(s) => s * s
}

val shapes: List[Shape] = List(Circle(2), Rectangle(3, 4), Square(5))
shapes.foreach(s => println(s"${s.getClass.getSimpleName} area: ${area(s)}"))

Expected output:

Circle area: 12.566370614359172
Rectangle area: 12.0
Square area: 25.0

Example 3: Comparing with Python dataclass

For context, let's see the Python equivalent:

from dataclasses import dataclass

@dataclass(frozen=True)
class Point:
    x: int
    y: int

p = Point(1, 2)
print(p)      # Point(x=1, y=2)
print(p.x)    # 1

In Scala, the case class version is more concise and gives you pattern matching for free.

Compare options / when to choose what

Both regular classes and case classes have their place. Here's a comparison to help you decide:

Feature Regular class Case class
Primary use Behavior-heavy objects with methods and mutable state Immutable data carriers
Boilerplate You write everything manually Automatically generated
Equality Reference equality unless overridden Structural equality
Pattern matching Manual extraction Built-in unapply
Copy method Not provided copy method
Immutability Not enforced (you can use var) Parameters are val by default
Inheritance Can be extended freely Prefer sealed hierarchy for exhaustive matching

When to use which

  • Use a case class when you need a simple data holder with automatic equality, hashing, and printing — e.g., DTOs, messages, configuration entries.
  • Use a regular class when you need complex behavior, incremental state changes, or custom methods that aren't just accessors.

Troubleshooting & edge cases

1. Case class parameters are val — you can't reassign them

If you try p.x = 5 where p is a case class instance, you'll get a compilation error. Use copy instead.

Wrong: p.x = 5

Correct: val p2 = p.copy(x = 5)

2. Case class equality with mutable fields

If you define a regular class with var fields and override equals/hashCode based on them, changing a field can break hash-based collections. Case classes avoid this by enforcing immutability.

3. Case classes and inheritance

Case classes can extend a trait or class, but they cannot extend another case class. If you need a hierarchy, use a sealed trait and case classes extending it.

4. copy method and default parameters

If you use default parameter values in a case class, the copy method preserves them. Be careful when copying with partial arguments — it might surprise you.

Example:

case class Point(x: Int = 0, y: Int = 0)
val p = Point(1, 2)
val p2 = p.copy() // uses current values, not defaults
println(p2) // Point(1,2)

5. Null and case classes

You can pass null to a case class parameter, but that's an anti-pattern. Prefer Option for optional fields.

What you learned & what's next

In this lesson, you learned how to create classes and case classes in Scala, and you saw how they compare to Python classes. You now understand:

  • The difference between a regular class and a case class
  • How to create both and use automatic features like copy, equality, and pattern matching
  • When to choose a case class over a regular class
  • Common pitfalls like immutability and inheritance restrictions

You also completed a hands-on exercise that modeled a simple domain with both types.

Next, you'll dive into pattern matching — the perfect companion to case classes. You'll learn how to destructure case classes, use guards, and make your code more expressive with match expressions.

Practice recap

Try creating your own case class hierarchy: a sealed trait Animal with case class Dog(name: String, age: Int) and case class Cat(name: String, lives: Int). Write a function that returns a friendly description for each using pattern matching. Then, use copy to update the age of a Dog and see how immutability plays out in practice.

Common mistakes

  • Trying to reassign a case class field: forgetting that case class parameters are val and getting a compilation error; use copy instead.
  • Using a regular class for simple data carriers: you end up writing boilerplate for equals, hashCode, and toString when a case class would do it for free.
  • Attempting to make one case class extend another — case classes cannot inherit from other case classes; use a sealed trait or abstract class instead.
  • Forgetting that case classes provide structural equality, so two instances with the same parameters are equal — if you need identity semantics, regular classes might be more appropriate.
  • Overusing case classes for highly mutable, behavior-centric objects — they're designed for immutable data; using var inside a case class is a code smell.

Variations

  1. Use @data class in Scala 3? Actually, Scala 3 still supports case class as the primary syntax, but you can also use enum for sealed hierarchies.
  2. For Java interoperability, consider regular classes when you need JavaBean-style getters and setters, or use @BeanProperty annotation on case class fields.
  3. If you need custom equality or hashing, override equals and hashCode in a regular class instead of using a case class.

Real-world use cases

  • Modeling domain events in a Scala backend service: immutable DTOs for API requests and responses, made simple with case classes and automatic JSON serialization.
  • Representing AST nodes in a DSL or compiler: case classes with pattern matching to evaluate or transform expressions safely and concisely.
  • Defining configuration entries in a Scala application: case classes with copy to derive new settings while preserving immutability.

Key takeaways

  • Case classes give you automatic equals, hashCode, toString, and copy — saving you boilerplate when creating data-holder classes.
  • Case class parameters are implicitly val, enforcing immutability — use copy to create modified instances.
  • Regular classes are for behavior-heavy objects with custom methods and mutable state.
  • Case classes integrate seamlessly with pattern matching, enabling expressive and safe code.
  • Choose case classes for DTOs, messages, and domain events; reserve regular classes for complex algorithms and services.
  • Avoid extending case classes; use sealed traits to build type hierarchies that support exhaustive pattern matching.

Sponsored

Sponsored

Discussion

Questions, corrections, and tips help everyone reading this page.

0 comments

Add a comment

Shown publicly with your comment.

Be constructive · max 4,000 characters

No comments yet — start the thread.

Related tutorials, quizzes, and articles for this topic.