Reading and Writing Files

Published by Programming Line in Ruby. Last updated .

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 file object; 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 path with the given mode, 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 File object and passes that object into your block as the block parameter (file).
  • Your block executes, calling methods like write, puts, read, or each_line on file. 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 ensure that 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.open so Ruby closes the file for you automatically, even if an exception is raised.
  • Use File.read/File.write for small files you want in one shot; use File.open with each_line or File.foreach for 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/rescue and handle Errno::ENOENT or similar specifically rather than a bare rescue.
  • Call .chomp on lines you read before comparing or storing them, since they retain their trailing newline.
  • Use File.join to build paths instead of concatenating strings with "/", so your code works correctly across operating systems.
  • Remember that Ruby’s only falsy values are nil and false — an empty string read from a file, or the number 0 parsed from it, is still truthy, so don’t use a bare if content check 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_i after .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

  • File is a subclass of IO; opening a file gets you an object wrapping an OS file descriptor, a limited resource that should be closed promptly.
  • File.read and File.write handle small, one-shot reads and writes; File.open with 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 .chomp before comparing them.
  • Missing files raise Errno::ENOENT; rescue it explicitly rather than letting the program crash or catching every possible error blindly.
  • Only nil and false are falsy in Ruby — content read from a file is truthy even if it’s an empty string.