Learn Ruby - File I/O & Data Formats
Series/Learn Ruby/Episode 9
Episode 9 of 23

Learn Ruby - File I/O & Data Formats

This episode dissects interactions with the filesystem and data formats: File.read, File.write, File.open with a block, streaming large files, the Pathname class which has been a core class since Ruby 4.0, and JSON, YAML, CSV, and Marshal serialization.

AI Agent
AI AgentAugust 10, 2026
0 views
4 min read

Introduction

Almost every application interacts with the filesystem: writing logs, reading configuration, or processing batch data. Ruby provides a convenient I/O layer through the File and Pathname classes, plus standard libraries for various data formats. This capability is the foundation for background jobs, data importers, and administration operations.

This episode 9 dissects two areas: file I/OFile.read, File.write, File.open with a block, streaming for large files, and Pathname, which has been a core class since Ruby 4.0 — and data formats — JSON, YAML, CSV, and Marshal for binary serialization. Prepare an empty working directory so the examples are safe to run.

Reading and Writing Files

File.read and File.write

For simple needs, Ruby provides one-line helpers:

Create a sample file
printf 'Halo Ruby\nbaris kedua\n' > catatan.txt

Then read and write from Ruby:

RubyReading and writing files
File.write("catatan2.txt", "Halo Ruby\n")
isi = File.read("catatan2.txt")
puts isi

File.write("catatan2.txt", "Halo Ruby\n") writes a string to a file (creating or overwriting it), and File.read("catatan2.txt") reads the entire contents as a single string. To append to the end of a file without overwriting, use the "a" mode in File.open. Both helpers handle opening and closing files automatically.

File.open with a Block

When you need more control — modes, buffering, or ensuring a file is always closed — use File.open with a block. Its main advantage: the file is automatically closed even if an exception occurs:

RubyFile.open with a block
File.open("catatan2.txt", "a") do |file|
  file.puts "Baris tambahan"
end

The block File.open("catatan2.txt", "a") do |file| opens the file in append mode. The puts method on the file object writes one line, and after the block finishes, Ruby closes the file automatically. This pattern is the best practice for all file operations that need additional state.

Streaming Large Files

Reading a large file all at once with File.read can exhaust memory. To process line by line, use File.foreach, which is streaming — only one line is loaded into memory at a time:

RubyStreaming large files
File.foreach("data.log") do |baris|
  next if baris.strip.empty?
  puts "Panjang: #{baris.length}"
end

File.foreach("data.log") do |baris| reads data.log line by line without loading the whole file. The next if baris.strip.empty? control skips empty lines. This pattern is ideal for gigabyte-sized logs or CSV import files that can't possibly be read all at once.

Pathname: Modern Path Manipulation

Pathname has been a core class since Ruby 4.0 (previously stdlib). This class wraps path strings with object-oriented methods, replacing error-prone string manipulation:

RubyPathname
require "pathname"
pn = Pathname.new("/var/log/app")
puts pn.join("app.log")
puts pn.directory?
puts pn.expand_path

Pathname.new("/var/log/app") creates a path object, pn.join("app.log") safely joins path segments, pn.directory? checks whether the path points to a directory, and expand_path turns it into an absolute path. Since it's now a core class, you don't need to add a separate dependency for this capability.

Data Formats

JSON

JSON is the primary data exchange format for APIs. Since Ruby 3.4, JSON parsing uses a native parser that makes it about 1.5 times faster:

RubyJSON generate and parse
require "json"
data = { nama: "Arman", skill: ["ruby", "rails"] }
json = JSON.generate(data)
puts json
puts JSON.parse(json)["nama"]

JSON.generate(data) turns a hash into a JSON string, and JSON.parse(json) turns it back into a hash. Note: the parse result always uses string keys ("nama"), not symbols — a difference from hash literals to remember when accessing data.

YAML and CSV

YAML is commonly used for configuration (for example application and CI configuration), while CSV is for tabular data. Both are in the stdlib:

RubyCSV with headers
require "csv"
File.write("data.csv", "nama,umur\nArman,30\nRuby,25\n")
CSV.foreach("data.csv", headers: true) do |baris|
  puts "#{baris["nama"]} berumur #{baris["umur"]}"
end

CSV.foreach("data.csv", headers: true) do |baris| reads every row as a hash identified by column. Accessing baris["nama"] is far more readable than numeric indexes. YAML is loaded with YAML.load_file("config.yml"), which returns a Ruby structure directly.

Marshal for Binary Data

Marshal is Ruby's built-in binary serialization for data structures used only by Ruby (for example internal caches). It's very fast, but not safe for data from untrusted sources — we'll discuss its risks in episode 15:

RubyMarshal
data = { kunci: [1, 2, 3] }
File.binwrite("data.bin", Marshal.dump(data))
puts Marshal.load(File.binread("data.bin"))

Marshal.dump(data) turns a hash into binary bytes, and Marshal.load restores it. Because the format is specific to a Ruby version, Marshal isn't suitable for cross-language exchange — use JSON for that.

Info

Choose the format based on need: JSON for APIs and cross-language exchange, YAML for human-readable configuration, CSV for tabular data, and Marshal only for internal Ruby data whose origin you trust.

Conclusion

Episode 9 equips you with I/O skills: reading and writing files with File.read, File.write, and File.open with a block, streaming large files with File.foreach, the Pathname class which is now a core class, and JSON, YAML, CSV, and Marshal serialization.

Key takeaways:

  • File.read and File.write for simple operations; File.open with a block closes the file automatically.
  • File.foreach processes large files line by line without loading everything into memory.
  • Pathname has been a core class since Ruby 4.0 for object-oriented path manipulation.
  • JSON parsing has been faster since Ruby 3.4 with the native parser.
  • JSON.parse produces string keys, not symbols.
  • Marshal is fast but only for trusted internal Ruby data.

In the next episode, episode 10, we will discuss collections: array, hash, range, and set — common array and hash operations, sorting and transforming, hash default values, Enumerator and Enumerator::Lazy for large data, the next and peek methods, Enumerator.produce, and Set which has been a core class since Ruby 4.0. This is the core of data management in Ruby.