Reading and Writing Files
Almost every real program eventually needs to read data from disk or save results to it — configuration files, logs, exported reports, cached data. Ruby handles this through the File class, which is a specialized subclass of the more general IO class. Once you understand the handful of modes a file can be opened in, and why the block form of File.open is almost always the right choice, working with files becomes routine and safe.
Overview / How It Works
In Ruby, File represents a connection to a file on disk, and it inherits nearly all of its behavior from IO, the class that models any stream of bytes (files, pipes, sockets). When you open a file, the operating system hands your process a file descriptor — a low-level handle to that open file — and Ruby wraps it in a File object you can call methods on. This is a limited resource: an operating system only allows a process to have so many files open at once, which is why closing files promptly matters.
Because everything in Ruby is an object (yes, even 1 and nil), a File object behaves like any other object: you can store it in a variable, pass it around, and call methods on it such as read, write, each_line, and close. Writes to a file are often buffered in memory before Ruby actually flushes them to disk, which is one reason closing (or using the block form, which closes for you) matters — an unclosed file may lose data that was never flushed.
Ruby gives you two levels of convenience. The class methods File.read and File.write open a file, do the entire operation, and close it again in one call — perfect for small files you want to slurp in one go. The instance-level API, opened with File.open, gives you finer control: reading line by line, appending, or writing incrementally without loading an entire large file into memory at once.
Syntax
The general form for controlled access is File.open with a block:
File.open(filename, mode) do |file|
# read from or write to `file` here
end
- filename — a string path, relative or absolute, to the file.
- mode — a string telling Ruby how you intend to use the file (see the table below).
- block — the code that uses the open
fileobject; Ruby automatically closes the file when the block ends, even if an exception is raised inside it.
| Mode | Meaning |
|---|---|
"r" |
Read-only. The file must already exist. The read pointer starts at the beginning. |
"w" |
Write-only. Creates the file if it doesn’t exist, and truncates (empties) it if it does. |
"a" |
Append. Creates the file if missing; every write is added to the end, regardless of the pointer position. |
"r+" |
Read and write. The file must already exist; the pointer starts at the beginning. |
"w+" |
Read and write, but truncates the file first. |
"a+" |
Read and append. You can read from anywhere, but writes always go to the end. |
Add a b to any mode (for example "rb" or "wb") to open the file in binary mode, which skips newline translation and is used for non-text data such as images.
Path helpers
Ruby’s File class also has small utility methods for working with paths without hand-rolling string concatenation: File.join("data", "users.txt") builds a path with the correct separator for the current OS, File.basename(path) returns just the filename, File.dirname(path) returns the directory portion, File.extname(path) returns the extension (like ".txt"), and File.exist?(path) returns true or false depending on whether something exists at that path.
Examples
Example 1: Writing and reading a whole file at once
File.write("greeting.txt", "Hello, Ruby!\n")
content = File.read("greeting.txt")
puts content
File.delete("greeting.txt")
Output:
Hello, Ruby!
File.write opens the file, writes the string, and closes it, returning the number of bytes written (which we ignore here). File.read opens it again, slurps the entire contents into one string, and closes it. Because the string we wrote already ends in "\n", puts doesn’t add a second newline — puts only appends a newline when the argument doesn’t already end with one. We finish by deleting the file so the script cleans up after itself.
Example 2: Writing several lines, then reading them back line by line
File.open("fruits.txt", "w") do |file|
file.puts "apple"
file.puts "banana"
file.puts "cherry"
end
File.open("fruits.txt", "r") do |file|
file.each_line do |line|
puts "Fruit: #{line.chomp}"
end
end
File.delete("fruits.txt")
puts File.exist?("fruits.txt")
Output:
Fruit: apple
Fruit: banana
Fruit: cherry
false
The first block opens fruits.txt in write mode and calls file.puts three times, each call adding one line plus a trailing newline. The block ends and Ruby closes the file automatically. The second block reopens it for reading and uses each_line to iterate over every line; each line still has its trailing "\n", so we call .chomp to strip it before interpolating. Finally we delete the file and confirm it’s gone with File.exist?, which returns false.
Example 3: Appending, reading all lines, and handling a missing file
File.write("log.txt", "Started\n")
File.open("log.txt", "a") do |file|
file.puts "Processing"
file.puts "Finished"
end
lines = File.readlines("log.txt")
puts "Total lines: #{lines.size}"
lines.each_with_index do |line, index|
puts "#{index + 1}: #{line.chomp}"
end
begin
File.read("missing_file.txt")
rescue Errno::ENOENT => e
puts "Error: file not found"
end
File.delete("log.txt")
Output:
Total lines: 3
1: Started
2: Processing
3: Finished
Error: file not found
File.write creates the file with one line, then the append block adds two more without disturbing the first. File.readlines reads the whole file at once and returns an array of strings, one per line (each still carrying its "\n"). Trying to read a file that doesn’t exist raises Errno::ENOENT, a real exception class, not just a nil return — wrapping the risky call in begin/rescue lets us handle it gracefully instead of crashing.
How It Works Step By Step
When you call File.open(path, mode) do |file| ... end, here is what happens, in order:
- Ruby asks the operating system to open the file at
pathwith the givenmode, which may create the file or fail if it doesn’t exist and the mode requires it to (like"r"). - The OS returns a file descriptor; Ruby wraps it in a
Fileobject and passes that object into your block as the block parameter (file). - Your block executes, calling methods like
write,puts,read, oreach_lineonfile. Writes may sit in an internal buffer rather than hitting disk immediately. - When the block finishes — whether normally or because an exception was raised inside it — Ruby runs an implicit
ensurethat flushes any buffered writes and closes the file descriptor, freeing it up for the OS to reuse. - Execution continues after the block with the file safely closed, which is why calling
File.exist?or reopening the same file right afterward always sees the finished result.
Common Mistakes
1. Opening without a block and forgetting to close
file = File.open("notes.txt", "w")
file.write("Some notes")
# file is never closed: buffered data may never reach disk,
# and the file descriptor stays open until the process exits
Without the block form, closing is entirely your responsibility. If you forget, buffered writes can be lost, and in a long-running program you can eventually exhaust the OS limit on open file descriptors. Prefer the block form, which closes the file for you automatically even if an exception occurs partway through:
File.open("notes.txt", "w") do |file|
file.write("Some notes")
end
2. Using “w” when you meant to append
File.write("history.txt", "Log start\n")
File.open("history.txt", "w") do |file|
file.puts "New entry"
end
puts File.read("history.txt")
File.delete("history.txt")
Output:
New entry
The second File.open call uses mode "w", which truncates the file before writing — the earlier "Log start" line is silently destroyed. This is a very common bug when someone means to keep adding to a log or history file but reaches for "w" out of habit. The fix is to use "a", which always writes at the end without erasing what’s already there:
File.write("history.txt", "Log start\n")
File.open("history.txt", "a") do |file|
file.puts "New entry"
end
puts File.read("history.txt")
File.delete("history.txt")
Output:
Log start
New entry
3. Forgetting that lines keep their trailing newline
line = "apple\n"
if line == "apple"
puts "Found apple"
else
puts "Not found"
end
Output:
Not found
Every line produced by each_line, readlines, or gets still has its trailing "\n" attached, so comparing it directly against a string literal without a newline always fails, even when the visible text matches. Strip it with .chomp before comparing:
line = "apple\n"
if line.chomp == "apple"
puts "Found apple"
else
puts "Not found"
end
Output:
Found apple
Best Practices
- Prefer the block form of
File.openso Ruby closes the file for you automatically, even if an exception is raised. - Use
File.read/File.writefor small files you want in one shot; useFile.openwitheach_lineorFile.foreachfor large files so you don’t load the entire thing into memory at once. - Double-check your mode string:
"w"truncates,"a"appends. Mixing these up is one of the most common file bugs. - Wrap file operations that might fail (a missing file, a permissions error) in
begin/rescueand handleErrno::ENOENTor similar specifically rather than a barerescue. - Call
.chompon lines you read before comparing or storing them, since they retain their trailing newline. - Use
File.jointo build paths instead of concatenating strings with"/", so your code works correctly across operating systems. - Remember that Ruby’s only falsy values are
nilandfalse— an empty string read from a file, or the number0parsed from it, is still truthy, so don’t use a bareif contentcheck to mean “file had data.”
Practice Exercises
- Write a script that writes the numbers 4, 9, and 15 to a file named
numbers.txt, one per line, then reads the file back, converts each line to an integer, and prints their sum. (Hint: use.to_iafter.chomp.) - Write a method
word_count(filename)that opens a file and returns the total number of words across all of its lines, where a “word” is anything separated by whitespace. Test it against a file you create in the same script. - Write a script that tries to read a file called
settings.txt. If it doesn’t exist, rescue the error and create it with a default value instead of crashing; then read and print the value.
Summary
Fileis a subclass ofIO; opening a file gets you an object wrapping an OS file descriptor, a limited resource that should be closed promptly.File.readandFile.writehandle small, one-shot reads and writes;File.openwith a block gives finer control and closes the file automatically when the block ends.- The mode string matters:
"r"reads,"w"truncates and writes,"a"appends — mixing up"w"and"a"is a classic way to lose data. - Lines from
each_line,readlines, and similar methods retain their trailing"\n"; use.chompbefore comparing them. - Missing files raise
Errno::ENOENT; rescue it explicitly rather than letting the program crash or catching every possible error blindly. - Only
nilandfalseare falsy in Ruby — content read from a file is truthy even if it’s an empty string.
