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.

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/O — File.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.
For simple needs, Ruby provides one-line helpers:
printf 'Halo Ruby\nbaris kedua\n' > catatan.txtThen read and write from Ruby:
File.write("catatan2.txt", "Halo Ruby\n")
isi = File.read("catatan2.txt")
puts isiFile.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.
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:
File.open("catatan2.txt", "a") do |file|
file.puts "Baris tambahan"
endThe 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.
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:
File.foreach("data.log") do |baris|
next if baris.strip.empty?
puts "Panjang: #{baris.length}"
endFile.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 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:
require "pathname"
pn = Pathname.new("/var/log/app")
puts pn.join("app.log")
puts pn.directory?
puts pn.expand_pathPathname.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.
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:
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 is commonly used for configuration (for example application and CI configuration), while CSV is for tabular data. Both are in the stdlib:
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"]}"
endCSV.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 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:
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.
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.parse produces string keys, not symbols.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.