Question

Common Ways to Read Files in Ruby: Methods, Trade-Offs, and Examples

rubyfile-io

Question

What are the common ways to read a file in Ruby, and what are the benefits and drawbacks of each approach?

For example, one approach is to open a file, read it one line at a time, and then close it:

file_obj = File.new($file_name, "r")

while (line = file_obj.gets)
  puts line
end

file_obj.close

Ruby provides several flexible file-reading APIs. When should each one be used, particularly with respect to memory use, readability, and ensuring that the file is closed?

Short Answer

Ruby can read an entire file at once, return its lines as an array, or process it incrementally. The best choice depends mainly on file size and whether you need the full contents in memory. You will also learn why block-based File.open is usually the safest default when working directly with file handles.

Concept

Ruby represents an open file with a File object, which is also an IO object. An IO object supplies methods such as read, gets, each_line, and readlines.

The central decision is eager reading versus streaming:

  • Eager reading loads all file content into memory immediately. Examples: File.read and File.readlines.
  • Streaming reads content gradually, usually one line at a time. Examples: File.foreach, file.each_line, and repeated file.gets.

Eager reading is convenient for small files because you can work with all content immediately. Streaming is safer for large files because Ruby does not need to hold the whole file in memory.

A second important concern is resource management. Opening a file uses an operating-system resource called a file descriptor. A file must eventually be closed. Ruby's block form of File.open closes the file automatically, including when an exception occurs, so it is commonly preferred over manually calling close.

Mental Model

Think of a file as a book.

  • File.read is like photocopying the entire book and placing every page on your desk. It is convenient for a short book, but a large book can fill the room.
  • File.readlines is like making a stack where each page is a separate item. This is useful when you need to refer to particular pages.
  • File.foreach and each_line are like reading one page, acting on it, and then putting it away before taking the next page. Your desk stays clear.
  • gets is like explicitly asking for the next page each time. It gives you maximum control over when reading happens.

The block form of File.open is like borrowing a book from a library with an automatic return system: Ruby returns (closes) it when the block finishes.

Take Quiz

Syntax and Examples

Read the entire file as one string

contents = File.read("notes.txt")
puts contents

File.read returns one String. It is concise and works well for small text files, configuration files, templates, and JSON documents.

Read all lines into an array

lines = File.readlines("notes.txt")

lines.each_with_index do |line, index|
  puts "#{index + 1}: #{line.chomp}"
end

File.readlines returns an Array of strings. Each line usually retains its ending newline, so chomp is often useful before display or comparison.

Stream a file one line at a time

File.foreach("notes.txt") do |line|
  puts line.chomp
end

File.foreach opens the file, yields each line, and closes the file when done. It is an excellent default for large line-oriented text files.

Open a file explicitly with automatic closing

File.open("notes.txt", "r") do |file|
  file.each_line do |line|
    puts line.chomp
  end
end

Use this form when you need an actual file object—for example, to combine reading methods, inspect the current position, or use a custom encoding.

Manually fetch each line with gets

File.open("notes.txt", "r") do |file|
  while (line = file.gets)
    puts line.chomp
  end
end

gets returns the next line, then returns nil at end of file. This is useful when your loop needs detailed control over when it reads the next line.

Read binary data

bytes = File.binread("image.png")
puts bytes.bytesize

Use File.binread for non-text data such as images, archives, and PDFs. It prevents text newline conversion and avoids interpreting the file using a text encoding.

Step by Step Execution

Consider this streaming example:

File.foreach("tasks.txt") do |line|
  task = line.chomp
  next if task.empty?

  puts "Task: #{task}"
end

Suppose tasks.txt contains:

Email client

Deploy application

Execution proceeds as follows:

  1. File.foreach opens tasks.txt.
  2. Ruby reads the first line, "Email client\n", and assigns it to line.
  3. chomp removes the trailing newline, so task becomes "Email client".
  4. The line is not empty, so Ruby prints Task: Email client.
  5. Ruby reads the next line, which contains only "\n".
  6. After chomp, task is an empty string. next skips the rest of this loop iteration.
  7. Ruby reads "Deploy application\n", removes its newline, and prints it.
  8. At end of file, File.foreach ends the loop and closes the file automatically.

Unlike File.readlines, the program holds only the current line (plus small overhead) while processing the file.

Real World Use Cases

  • Application configuration: Use File.read for a small JSON, YAML, or text configuration file before parsing it.
  • Log processing: Use File.foreach to scan a multi-gigabyte server log for errors without exhausting memory.
  • CSV-like imports: Stream rows one at a time, validate each row, and save valid records to a database.
  • Template loading: Read a small email or HTML template into a string with File.read.
  • Line-numbered tools: Use File.readlines when a small source file must be edited or accessed by line number.
  • Binary uploads and checksums: Use File.binread when raw bytes matter, such as calculating a digest for a downloaded file.
Take Quiz

Real Codebase Usage

In production Ruby code, developers usually choose an API that makes resource handling and memory behavior obvious.

Prefer a block when opening a handle

File.open(path, "r", encoding: "UTF-8") do |file|
  file.each_line do |line|
    process(line)
  end
end

The block ensures close is called even if process raises an exception.

Use guard clauses while streaming

File.foreach(path, encoding: "UTF-8") do |line|
  record = line.strip
  next if record.empty?
  next if record.start_with?("#")

  import_record(record)
end

This keeps the main processing path simple and avoids deeply nested if statements.

Handle expected file errors

begin
  contents = File.read(path, encoding: "UTF-8")
rescue Errno::ENOENT
  warn "File not found: #{path}"
rescue Errno::EACCES
  warn "Permission denied: #{path}"
end

Do not silently ignore file errors. A missing file and a permission problem usually need different responses.

Select an encoding deliberately

File.foreach(path, encoding: "UTF-8") do |line|
  # Process text expected to be UTF-8.
end

When a project has a known file format, specifying its encoding makes failures easier to diagnose and reduces environment-dependent behavior.

Common Mistakes

Forgetting to close a manually opened file

file = File.open("notes.txt", "r")
puts file.read
# file.close is missing

This can leave file descriptors open, especially in a long-running process. Prefer a block:

File.open("notes.txt", "r") do |file|
  puts file.read
end

Loading a huge file with File.read or File.readlines

lines = File.readlines("very-large.log")

This creates an array containing every line, which can consume substantial memory. Stream it instead:

File.foreach("very-large.log") do |line|
  process(line)
end

Using inconsistent variable names

The original style can accidentally create one variable and use another:

fileObj = File.new($file_name, "r")
while (line = file_obj.gets)
  puts line
end

Ruby treats fileObj and file_obj as different variables. Ruby convention also favors snake case:

file_obj = File.new($file_name, "r")

Forgetting that lines include newline characters

File.foreach("names.txt") do |line|
  puts "Hello, #{line}!"
end

If line ends in \n, the exclamation mark appears on the next line. Use chomp:

puts "Hello, #{line.chomp}!"

Using text reading for binary content

Avoid treating images, compressed files, or arbitrary bytes as ordinary text. Use File.binread or open with binary mode ("rb") when byte-for-byte preservation matters.

Assuming a file always exists

File.read and related methods raise Errno::ENOENT when the path does not exist. Validate user-provided paths when appropriate and handle errors where the application can make a useful decision.

Comparisons

ApproachReturnsMemory useAutomatically closes?Best for
File.read(path)One StringHigh for large filesYesSmall files needed as complete text
File.binread(path)One binary StringHigh for large filesYesSmall binary files or raw bytes
File.readlines(path)Array of linesHigh for large filesYesSmall files when you need all lines or indexes
File.foreach(path) { ... }Iterates linesLowYesLarge text files processed line by line
File.open(path) { |f| ... }A File object in the blockDepends on methods usedYesCustom reading logic and options
file.each_lineIterates linesLowOnly with block-based File.openLine processing using an open handle
file.gets in a loopOne line per callLowOnly with block-based File.openReading controlled by custom loop logic

File.new(path, "r") and File.open(path, "r") both create file objects. The practical difference is that File.open accepts a block and automatically closes the file afterward. Prefer File.open when you need to open a file yourself.

each_line and gets both stream lines. Choose each_line for straightforward iteration; choose gets when each read must happen at a precise point in a more complex loop.

Take Quiz

Cheat Sheet

# Entire small text file
text = File.read(path, encoding: "UTF-8")

# Entire small file as an array of lines
lines = File.readlines(path, encoding: "UTF-8")

# Preferred line-by-line processing
File.foreach(path, encoding: "UTF-8") do |line|
  puts line.chomp
end

# Custom work with a safely managed file object
File.open(path, "r", encoding: "UTF-8") do |file|
  file.each_line { |line| process(line) }
end

# Explicitly request one line at a time
File.open(path, "r") do |file|
  while (line = file.gets)
    process(line)
  end
end

# Raw binary bytes
bytes = File.binread(path)
  • Use File.read for small text files.
  • Use File.readlines only when you really need an array containing every line.
  • Use File.foreach for large line-based files.
  • Prefer File.open { |file| ... } over manual open and close.
  • gets returns nil at end of file.
  • chomp removes a record separator, commonly the trailing newline.
  • Text and binary files have different needs; use binread or "rb" for raw bytes.

FAQ

What is the easiest way to read a file in Ruby?

For a small text file, use File.read("path/to/file.txt"). It returns the entire contents as one string.

What is the best Ruby method for reading a large file?

Use File.foreach(path) { |line| ... } to process one line at a time. It avoids loading the complete file into memory.

Does File.readlines close the file automatically?

Yes. File.readlines opens the file internally, reads the lines, and closes it before returning the array.

Should I use File.new or File.open in Ruby?

Prefer File.open with a block. It automatically closes the file when the block ends, including if an exception is raised.

Why does Ruby add a blank line when I print a line from a file?

The line likely already ends with \n, and puts adds another newline. Use puts line.chomp when you want to remove the existing line ending.

When should I use gets instead of each_line?

Use each_line for normal line iteration. Use gets when you need manual control over exactly when the next line is read.

How do I read a binary file in Ruby?

Use File.binread(path) for a small binary file, or File.open(path, "rb") when streaming binary data.

What happens if Ruby cannot find the file?

Ruby raises Errno::ENOENT. Handle it with rescue when your program can recover or show a helpful message.

Take Quiz

Mini Project

Description

Build a small Ruby log scanner that reads an application log one line at a time and reports error entries. This demonstrates streaming file processing, newline cleanup, guard clauses, and safe file access without loading the full log into memory.

Goal

Print every non-empty log line containing ERROR, followed by the total number of errors found.

Requirements

Use File.foreach to read the file line by line. Ignore blank lines. Match error entries containing the text ERROR. Print each matching entry without its trailing newline. Print the final error count. Handle a missing file with a helpful message.

Take Quiz

Keep learning

Related questions

View more