Read and Write Files in Scala

Learn to read and write files using Scala in this hands-on tutorial for Python developers. Master file I/O with practical examples and clear explanations.

Focus: read and write files using scala

Sponsored

You know the drill from Python: open(), read(), write(), close() — but Scala's file I/O looks different, and if you try to treat it like Python you'll hit confusing errors like java.io.FileNotFoundException or wonder why scala.io.Source is not closing your file. The pain is real: you need to read configuration files, write logs, or process CSV data in Scala, and there's no single built-in 'file object' like in Python. This lesson gives you a clear mental model and practical patterns so you can read and write files using Scala confidently and idiomatically.

The problem this lesson solves

When you need to read and write files using Scala, the standard library offers flexible but fragmented APIs. Unlike Python's single open() function, Scala splits file handling across scala.io.Source, java.nio.file, and java.io — and knowing which one to use for what task is the first hurdle.

Common pain points you'll hit without this lesson: - Forgetting to close resources — Python's context manager (with open(...)) is automatic; Scala's Source is not. Leak file handles and you'll crash on a long-running job. - Encoding surprises — reading a UTF-8 file as the platform default can corrupt text. - Path confusionjava.io.File vs java.nio.file.Path can trip you up. - Verbose boilerplate — writing a file with just java.io.PrintWriter feels clunky compared to Python's one-liner. - Performance traps — using Source.fromFile(...).getLines naively can blow up memory on huge files.

This lesson solves those problems with a clear mental model, step-by-step approaches, and a hands-on project.

Core concept / mental model

Think of Scala file I/O in two layers, like Python's open() vs io modules:

  1. The legacy/JVM layerjava.io and scala.io.Source. These are like Python's open() — good for small-to-medium text files, line-by-line reading, and simple writing. scala.io.Source is Scala's own friendly wrapper for reading.
  2. The modern/NIO layerjava.nio.file. This is like Python's pathlib — designed for robust, production-grade file operations with better performance and error handling. It's the recommended choice for new code, especially large files.

A useful analogy: scala.io.Source is your quick open() with a for loop, while java.nio.file is your pathlib.Path with built-in methods like readAllLines and write.

Key definitions: - scala.io.Source — an iterator over characters/lines in a file. One-shot; you must close it. - java.nio.file.Files — static methods for reading/writing all bytes, lines, or streams. - java.io.PrintWriter — a writer for formatted text output, similar to Python's print to a file.

How it works step by step

Step 1 – Choose your layer

  • For reading a small text file into memory: scala.io.Source or java.nio.file.Files.readAllLines.
  • For writing a small text file: java.nio.file.Files.write (simplest) or java.io.PrintWriter (for formatted output).
  • For large files: use Source.getLines or Files.newBufferedReader in a stream/loop — never read all lines into memory.

Step 2 – Manage resources

Scala 2.13+ has scala.util.Using for automatic resource management — the equivalent of Python's with statement. Use it to close Source or BufferedReader automatically.

import scala.util.Using

Using(scala.io.Source.fromFile("data.txt")) { source =>
  val content = source.mkString
  // do something with content
}

Step 3 – Read lines

For line-by-line processing, use getLines to get an iterator. This is memory-efficient.

Step 4 – Write text

Use Files.write with java.nio.file.Path for a simple write (overwrites or appends). For formatted output, use PrintWriter inside a Using block.

Step 5 – Handle encoding

Always specify the charset (e.g., "UTF-8") explicitly to avoid platform dependencies.

Step 6 – Handle errors

Wrap I/O in try-catch or use Try to handle FileNotFoundException, IOException, etc.

Hands-on walkthrough

Exercise 1: Read and write a simple text file with scala.io.Source and java.nio.file

import scala.util.Using
import java.nio.file.{Files, Paths}
import java.nio.charset.StandardCharsets

// Write a file
val path = Paths.get("hello.txt")
Files.write(path, "Hello, Scala!\n".getBytes(StandardCharsets.UTF_8))

// Read it back
Using(scala.io.Source.fromFile(path.toFile)(scala.io.Codec.UTF8)) { source =>
  val content = source.mkString
  println(content) // Prints: Hello, Scala!
}

Output:

Hello, Scala!

Exercise 2: Read a large file line by line and write a transformed file

import scala.util.Using
import java.io.PrintWriter
import java.nio.charset.StandardCharsets

// Read line by line, uppercase, write out
Using(scala.io.Source.fromFile("input.txt")(scala.io.Codec.UTF8)) { source =>
  Using(new PrintWriter("output.txt", "UTF-8")) { writer =>
    source.getLines.foreach(line => writer.println(line.toUpperCase))
  }
}

If input.txt contains line1 and line2, output.txt will contain LINE1 and LINE2.

Exercise 3: Use java.nio.file.Files for a one-shot read/write (simplest for small files)

import java.nio.file.{Files, Paths}
import java.nio.charset.StandardCharsets
import scala.jdk.CollectionConverters._

// Read all lines into a list
val path = Paths.get("config.properties")
val lines = Files.readAllLines(path, StandardCharsets.UTF_8).asScala.toList

// Write all lines back (overwrite)
Files.write(path, lines.asJava, StandardCharsets.UTF_8)

Exercise 4: Error handling with Try

import scala.util.{Try, Using}
import scala.io.Source

def readFile(fileName: String): Try[String] = Try {
  Using(Source.fromFile(fileName)(scala.io.Codec.UTF8)) { source =>
    source.getLines.mkString("\n")
  }.get
}

readFile("missing.txt") match {
  case scala.util.Success(content) => println(content)
  case scala.util.Failure(e) => println(s"Failed: ${e.getMessage}")
}

Expected output for missing file:

Failed: missing.txt (No such file or directory)

Pro tip: Always use Using to close resources automatically — just like Python's with. Never open a Source without closing it.

Compare options / when to choose what

Task Python Scala (recommended) When to use it
Read small file into string open(...).read() Files.readString(path, charset) Small configs, simple inputs
Read all lines readlines() Files.readAllLines(...) Small files, quick scripts
Read line by line for line in f Source.fromFile(...).getLines Large files, streaming
Write string open(..., 'w').write(...) Files.write(path, bytes, ...) Simple writes
Write formatted text print(..., file=f) PrintWriter Logging, CSV output
Append open(..., 'a') Files.write(path, bytes, StandardOpenOption.APPEND) Log files

When to choose what: - Use Files for most new code — it's concise, modern, and handles large files well. - Use Source when you want a Scala-native feel or need getLines with minimal ceremony. - Use PrintWriter for formatted output, like printf-style. - For binary files, use Files.readAllBytes / Files.write.

Troubleshooting & edge cases

Error: java.io.FileNotFoundException

Cause: File path doesn't exist, or working directory is different from where you run the app. Fix: print System.getProperty("user.dir") to see the working directory, and use absolute paths if needed.

Error: Garbled characters (mojibake) when reading/writing

Cause: Encoding mismatch. Fix: always specify charset explicitly, e.g., scala.io.Codec.UTF8 or StandardCharsets.UTF_8.

Error: OutOfMemoryError when reading a huge file

Cause: Calling mkString or readAllLines on a massive file. Fix: process line by line with getLines and never store all lines in memory.

Edge case: File not closed because you used Source directly without Using

Symptom: File lock on Windows, or “too many open files” on Unix. Fix: wrap in Using or call .close() in a finally block.

Edge case: Empty or missing lines

getLines strips line terminators, so empty lines become empty strings. Handle them if you need them.

What you learned & what's next

You've mastered how to read and write files using Scala: - Core idea: Use scala.io.Source for reading, java.nio.file.Files for modern I/O, and PrintWriter for formatted writing. - Hands-on exercises: You wrote and read text files, processed lines, used Using for resource safety, and handled errors with Try. - Mental model: Two layers — legacy vs NIO — and when to choose each. - Troubleshooting: You can debug encoding, missing files, and memory issues.

Now you're ready to move to the next lesson in the track, where you'll build on this foundation — perhaps serializing data structures or working with external libraries for more advanced I/O. Keep experimenting: try reading CSV, appending to logs, or processing a large dataset line by line.

Key takeaway: In Scala, file I/O is about choosing the right tool and managing resources — not just calling open(). Master these patterns and you'll handle files like a pro.

Practice recap

Write a Scala script that reads a CSV file, filters rows based on a condition (e.g., age > 30), and writes the matching rows to a new CSV file. Use Using for both reading and writing, and specify UTF-8 encoding. Test it with a sample file to see the output.

Common mistakes

  • Forgetting to close scala.io.Source — causes resource leaks; always use Using or finally.
  • Not specifying encoding — leads to garbled text; always pass Codec.UTF8 or StandardCharsets.UTF_8.
  • Reading the entire large file into memory with mkString or readAllLines — instead use getLines for streaming.
  • Using java.io.File without checking existence — causes FileNotFoundException; verify or catch it.

Variations

  1. Use scala.io.Source for simple reading vs java.nio.file.Files for modern, robust I/O.
  2. Use java.io.PrintWriter for formatted writing vs Files.write for one-shot bytes.
  3. Use third-party libraries like os-lib (https://github.com/com-lihaoyi/os-lib) for even simpler file operations.

Real-world use cases

  • Reading configuration files (e.g., .properties or .conf) at application startup to set parameters.
  • Processing large log files line by line to extract errors or aggregate statistics without exhausting memory.
  • Writing export files (CSV, JSON) from a Scala backend to send data to other systems.

Key takeaways

  • Scala offers multiple file APIs; choose scala.io.Source for reading text, java.nio.file.Files for modern I/O, and PrintWriter for formatted output.
  • Always manage resources with scala.util.Using to avoid leaks, just like Python's with.
  • Explicitly specify the character encoding (UTF-8) to prevent mojibake.
  • For large files, process line by line with getLines instead of reading everything into memory.
  • Handle errors gracefully using Try or catch to avoid crashes on missing files or permission issues.

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.