Reading and writing files in Ruby
Fast recall
Quick revision
Core answer
Whether the learner can choose whole-file or streaming I/O and manage resources safely.
Using w when append was intended, loading an unbounded file into memory, or leaving a handle open.
Discuss block cleanup, access modes, encoding, atomic replacement, and untrusted paths.
Interview-ready guide
Interview answer
Ruby's File class inherits from IO and provides both convenience and streaming operations. File.read(path) returns a whole file as a string, while File.write(path, text) writes a complete string. They are clear when the data is known to be small and bounded.
For larger input, File.foreach(path) yields one line at a time. When I need a handle, encoding, or several writes, I use File.open(path, mode) with a block. Ruby closes that handle when the block exits, including when an exception is raised.
The mode must be deliberate. r reads an existing file, w creates or truncates before writing, and a preserves existing content and appends at the end. For example, an audit log can open in append mode, while replacing a generated report should not append yesterday's report.
File operations can fail because a path is missing, permissions deny access, storage is full, bytes do not match the expected encoding, or input is larger than expected. I rescue only errors the application can handle and keep the original exception context for logging.
Paths from users need a controlled base directory and filename policy; simple string concatenation can enable path traversal. For important replacement files, I write a temporary file in the same filesystem, flush it as required, and rename it into place so readers do not observe half-written output. I test missing, malformed, and oversized input as separate failure cases.
Reading and writing files in Ruby
Direct Answer
Ruby offers convenience methods such as File.read, File.write, and File.foreach, plus File.open for controlled access modes and streaming. Use the block form of File.open so Ruby closes the handle even when an exception occurs, choose r, w, or a deliberately, and treat paths, encoding, size, and partial failure as input concerns.
Deep Dive & Architecture
Opening a file asks the operating system for a handle with specific permissions and an initial position. Read mode starts at the beginning and rejects writing. Write mode permits output but truncates an existing file as part of opening it. Append mode keeps existing bytes and places writes at the end. The plus variants allow both reading and writing, while binary mode prevents text transformations that matter on some platforms.
File.open without a block returns a handle that the caller must close, usually with an ensure clause. The block form expresses ownership more safely: Ruby yields the handle, then closes it while leaving the block even if parsing or writing raises. File.read and File.write manage their short-lived handles internally. For line-oriented data, File.foreach avoids constructing one large string and naturally supports early filtering.
Encoding is part of the input contract. A text file may declare or require UTF-8, contain a byte-order mark, or include invalid bytes. Pass an explicit encoding when the source is known and decide whether invalid input should be rejected or transcoded. Binary images and archives should use binary mode and should not be treated as encoded text.
Failure can occur after only part of a write reaches storage. When a file must appear atomically, write the complete content to a temporary file in the destination directory, set permissions, flush or fsync when durability matters, and rename it over the target. Same-filesystem rename is commonly atomic, but application requirements still decide backup and recovery policy.
Security starts before opening. Resolve requested paths against an approved base, reject traversal outside that root, avoid following unexpected symbolic links where the threat model requires it, limit file size, and do not trust an extension as a content check. Rescue narrow exceptions such as Errno::ENOENT only where the program has a meaningful fallback; otherwise let the caller receive a useful failure rather than silently returning incomplete data.
Concurrency adds one more boundary: two writers can still overwrite or interleave data even when both close their handles correctly. Use file locking only with understood platform semantics, or move shared mutable records to storage designed for concurrent transactions.
Code Example
require "tmpdir"
Dir.mktmpdir do |directory|
source = File.join(directory, "scores.txt")
report = File.join(directory, "passed.txt")
File.write(source, "Ada,91\nLin,67\nSam,84\n", mode: "w:UTF-8")
File.open(report, "w:UTF-8") do |output|
File.foreach(source, encoding: "UTF-8") do |line|
name, score = line.strip.split(",", 2)
output.puts(name) if Integer(score, 10) >= 80
end
end
raise "unexpected report" unless File.read(report) == "Ada\nSam\n"
end
The input is streamed line by line; the output handle closes automatically when its block ends.
Common Mistakes & Pitfalls
Using w when append was intended, loading an unbounded file into memory, or leaving a handle open.
Check your understanding
Answer these without looking back at the article.
- What is the difference between w and a file modes?
- When should you use File.foreach instead of File.read?
- How would you prevent path traversal in a download endpoint?
Looking for a structured roadmap?
Every module on this page is also part of the full Java backend interview track.