Scala's object Keyword

Understand Scala's object keyword — Scala for Python Developers.

Focus: understand scala's object keyword

Sponsored

You've mastered Scala's classes, case classes, and traits — but there's a strange keyword lurking in every Scala codebase that looks like a class, acts like a singleton, and yet has no Python equivalent. When you first see object Main extends App, your Python instincts scream 'that's just a class with static methods!' — and that instinct will lead you astray. Scala's object keyword creates a singleton object: a type with exactly one instance, lazily initialized on first access, that serves as your home for static-like methods, factory methods, and the entry point of your entire program. Without understanding object, you'll find yourself writing unnatural Scala code, fighting the compiler, and missing out on one of the language's most elegant design patterns.

The problem this lesson solves

In Python, you have several ways to organize code that doesn't belong to a specific instance. You create a class with @staticmethod, use module-level functions, or define a module-level singleton with a custom __new__ override. Each approach has trade-offs, but none is canonical — your codebase might mix all three, and reviewers will debate which is 'Pythonic' at every code review.

Scala solves this problem decisively with the object keyword. An object is a singleton: a type with exactly one instance, automatically created and managed by the JVM. You never call new on it — you just reference it by name, and the Scala compiler ensures only one instance exists across your entire application.

Here's the pain point for Python developers: you cannot translate object to any single Python concept. It's simultaneously a static method holder, a class with a pre-created instance, a module-level function namespace, and a lazy-initialized singleton. If you try to map it to your Python mental model, you'll write code that compiles but feels wrong. This lesson gives you the correct mental model so you write idiomatic Scala from day one.

Core concept / mental model

The simplest way to think about object in Scala is this: an object is a class that Scala has already instantiated for you, exactly once, and named the same as the class.

Think of a Python module-level singleton:

class _DatabaseConnection:
    _instance = None
    def __new__(cls):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
            cls._instance.connected = False
        return cls._instance

database = _DatabaseConnection()

Scala's object gives you this pattern built into the language, with lazy initialization (the instance is created on first access, not at program start) and thread safety (the JVM guarantees only one instance, even under concurrent access).

But object goes beyond singletons. It also serves three distinct roles:

  1. A namespace for static-like methods — the Scala equivalent of Python's @staticmethod and module-level functions. When you see Math.sqrt(2) in Scala, Math is an object.
  2. A companion to a class — an object with the same name as a class shares private access with that class. This is where factory methods like apply() live.
  3. A program entry point — your main method lives inside an object, not a class.

Pro tip: If a method doesn't need instance state, it should probably live in an object — not a class. This is idiomatic Scala, and it's how you keep your class definitions focused on state and behavior that genuinely requires an instance.

How it works step by step

Let's trace exactly what happens when the JVM encounters a Scala object:

  1. Compilation: The Scala compiler generates a Java class with a private constructor and a public static final field holding the single instance. This is the JVM-level singleton pattern, generated automatically.
  2. Lazy initialization: The instance is not created at class-load time. Instead, the static field is initialized on first access, using a thread-safe lazy initialization mechanism (simplified: a static inner class holding the instance, or a synchronized check).
  3. First access: When your code references the object by name (e.g., Greeting.sayHello()), the JVM triggers the static initializer, creates the single instance, and assigns it to the static field. All subsequent references reuse that same instance.
  4. Usage: Methods on the object are essentially static methods under the hood, but they're called with instance-method syntax — Greeting.sayHello() looks like a class method call, but it's really a static call on the singleton.

Here's what this means in practice:

# Python: careful about when your singleton is created
import datetime

class Config:
    _instance = None
    def __new__(cls):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
            cls._instance.load()
        return cls._instance
    def load(self):
        self.timeout = 30
// Scala: lazy, thread-safe, automatic
object Config {
  val timeout: Int = 30
}
// Config is created only when you first mention it:
// println(Config.timeout)

Hands-on walkthrough

Let's build a complete example that exercises all the roles of object. We'll create a temperature converter — something you might build in Python as a module of utility functions.

Step 1: Create a simple object with methods.

Create a file TempConverter.scala:

object TempConverter {
  def celsiusToFahrenheit(c: Double): Double = c * 9.0 / 5.0 + 32.0
  def fahrenheitToCelsius(f: Double): Double = (f - 32.0) * 5.0 / 9.0
  val boilingPointC: Double = 100.0
}

// In a separate file, or in the REPL:
println(TempConverter.celsiusToFahrenheit(0))   // 32.0
println(TempConverter.fahrenheitToCelsius(212)) // 100.0
println(TempConverter.boilingPointC)            // 100.0

Run it with scala TempConverter.scala (or paste into the Scala REPL). You'll see:

32.0
100.0
100.0

Step 2: Use a companion object for factory methods.

The most common Scala pattern is a class and a companion object with the same name. The companion can access the class's private members:

class User private (val name: String, val age: Int) {
  // private constructor — only the companion can call it
}

object User {
  def apply(name: String, age: Int): User = new User(name, age)
  def fromJson(json: String): User = {
    // simplified: parse name and age from a JSON string
    User("parsed-name", 25)
  }
  val anonymous: User = new User("guest", 0)
}

// Usage — note: no `new` keyword!
val alice = User("Alice", 30)
val bob = User.fromJson("{\"name\":\"Bob\",\"age\":40}")
val guest = User.anonymous

Step 3: Create an application entry point.

Every Scala program needs a main method, and it must live in an object:

object MainApp {
  def main(args: Array[String]): Unit = {
    val name = if (args.length > 0) args(0) else "world"
    println(s"Hello, $name!")
  }
}

Compile and run with scala MainApp.scala Scala — you'll see Hello, Scala!.

Pro tip: The syntax User("Alice", 30) works because the companion object defines an apply method. This is so common that Scala has a shorthand: case class automatically generates a companion object with an apply method. That's why you can write case class Person(name: String) and then Person("Alice") without new.

Compare options / when to choose what

As a Python developer, you have multiple ways to achieve similar results. Here's a comparison table to help you map your instincts:

What you want Python approach Scala approach When it's the right choice
Utility functions without state Module-level functions or @staticmethod object with methods Almost always — it's the idiomatic Scala way
A single global instance __new__ singleton pattern, module-level variable object Always — it's built-in and thread-safe
Factory methods for a class @classmethod like from_json Companion object with apply or named factory methods When you need controlled instance creation
Constants Module-level constants object with val fields When constants are related, group them in an object
A class with only static behavior Class with all static methods object (not a class!) Always — you don't need a class if you never instantiate it
Thread-safe lazy singleton Custom __new__ + lock object Always — the JVM handles it

The key decision rule: If you would write a Python class with only @staticmethod and @classmethod methods, or a module full of functions and constants, then in Scala you should write an object. If you need multiple instances with state, use a class. If you need both, use a class and a companion object.

There's also an alternative Scala pattern worth knowing: package objects (deprecated in Scala 3 in favor of top-level definitions). In Scala 2, you could write package object mypkg to hold functions and constants at the package level. In Scala 3, you simply write top-level def and val in a file — no enclosing object needed. This works well for small utility functions, but for anything cohesive, an object is still more discoverable and testable.

Troubleshooting & edge cases

Error: object cannot be extended (no extends). An object is final — it cannot be subclassed. If you see a compiler error like illegal inheritance from final object, you're trying to extend an object. Instead, extract the shared behavior into a trait and have both the object and any classes extend it:

trait Formatter {
  def format(s: String): String
}
object UpperFormatter extends Formatter {
  def format(s: String): String = s.toUpperCase
}

Gotcha: object initialization order. An object is initialized lazily on first access, so if your object's initialization reads configuration or connects to a database, that work happens when the object is first touched — not at program start. This is usually fine, but it can cause surprising lag in the first call that uses the object.

Gotcha: circular references between objects. Two objects that reference each other during initialization can cause NullPointerException if A's initialization uses B before B is fully initialized. Solution: initialize dependencies lazily inside methods rather than in val field declarations.

Gotcha: main must be in an object. A common mistake is placing main inside a class:

class MainApp {  // WRONG
  def main(args: Array[String]): Unit = println("Hi")
}

This compiles but doesn't run as an application — scala MainApp will tell you that it found no main method. The main must be in an object, or you can extend App (though App has its own quirks with argument handling).

Gotcha: object vs. val singleton comparison. If you need a singleton that's part of a larger inheritance hierarchy, a val in a companion object might be easier to work with:

class Database(host: String)
object AppDatabase {
  val main: Database = new Database("localhost")
}
// vs.
object MainDatabase extends Database("localhost")

The val approach lets you refer to the singleton as AppDatabase.main and swap implementations more easily.

What you learned & what's next

You now understand why every Scala codebase is full of object declarations. An object is Scala's built-in singleton — a type with exactly one lazily-initialized, thread-safe instance. You learned that it serves three critical roles: a home for static-like utility methods, a companion to classes for factory methods and private access, and the mandatory container for your main entry point. You can now map Python patterns to Scala idioms: Python module-level functions become object methods, Python singleton hacks become built-in objects, and Python class methods become companion object factory methods.

You're ready to apply this immediately: refactor a small Python utility module into idiomatic Scala using object. The next lesson in this track examines pattern matching — Scala's powerful replacement for Python's if/elif chains and match statements. You'll see how object and case classes work together to enable elegant, exhaustive pattern matching that the compiler verifies for you.

Practice recap

Create a Calculator object with add, subtract, multiply, and divide methods, mirroring a Python module you might write for arithmetic. Then define a companion class Calculator with a constructor field precision: Int and an apply method in the object that defaults to precision 2. Verify that Calculator(5, 3) and Calculator.apply(5, 3) behave identically, and confirm divide throws ArithmeticException on division by zero — the Scala way.

Common mistakes

  • Treating object like a class and trying to extend it — object is final and cannot be subclassed; extract shared behavior into a trait instead.
  • Placing the main method inside a class — the JVM entry point must live in an object (or via extends App, with its own caveats).
  • Using new with an object: new MyObject fails — the singleton already exists; reference it by name directly.
  • Ignoring lazy initialization and assuming an object is created at program start — initialization happens on first access, which can cause surprising delays or ordering issues.

Variations

  1. Use extends App for a quick main — object Hello extends App { println(\"Hi\") } — but beware that args access differs from a custom main method, especially with JVM flags.
  2. Scala 3 allows top-level definitions without an enclosing object for utility functions, though a cohesive object remains the clearer pattern in many cases.
  3. Package objects in Scala 2 serve a similar role for package-level utilities, but are deprecated in Scala 3 in favor of top-level definitions.

Real-world use cases

  • A global configuration holder in a microservice — e.g., object Settings loads environment variables and provides typed vals used across the service.
  • A companion object for a case class to provide apply, fromJson, and other factory methods without exposing the constructor — common in domain models.
  • A utility library for logging or math operations where no state is needed — e.g., object Logger aggregating log methods, similar to Python's logging module functions.

Key takeaways

  • An object is a singleton — the Scala compiler enforces exactly one instance, created lazily on first access with thread safety built in.
  • Use object for static-like utility methods and constants; use class for anything that requires multiple instances with state.
  • A companion object shares the same name and private access with its class, enabling factory methods like apply that hide constructors.
  • Every Scala application entry point — def main(args: Array[String]) — must live inside an object.
  • If you'd write a Python class with only static/class methods, or a module full of functions and constants, the idiomatic Scala equivalent is an object.

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.