File I/O

Reviewed & published by Brayan K

Ruby is a dynamic, beginner-friendly programming language, and file input/output lets your programs persist data — reading from and writing to files on disk so work survives between runs.

Part of the free Ruby course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

By the end of this lesson you'll write, read, and append files safely with the block form, and process them line by line.

What You'll Learn in This Lesson

1️⃣ Writing, Reading, and Appending

Open a file with File.open(name, mode) and a block — the file closes automatically when the block ends. Use "w" to write (overwrites), "a" to append, and read with File.read or File.readlines.

# WRITE: "w" mode creates (or overwrites) a file.
# The block form auto-closes the file for you — always prefer it.
File.open("notes.txt", "w") do |file|
  file.puts "First line"
  file.puts "Second line"
end

# READ the whole file into one string:
content = File.read("notes.txt")
puts content
# -> First line
# -> Second line

# Read all lines into an ARRAY (one string per line):
lines = File.readlines("notes.txt")
puts "Line count: #{lines.length}"   # -> Line count: 2

# APPEND with "a" mode (adds without erasing):
File.open("notes.txt", "a") do |file|
  file.puts "Third line"
end
puts File.read("notes.txt").lines.count   # -> 3

2️⃣ Processing Files Line by Line

For larger files, process one line at a time with each_line or File.foreach so you don't load everything into memory. Call chomp to drop the trailing newline, and check files first with File.exist?.

File.write("notes.txt", "first line\nsecond line\nthird line\n")   # the file this example reads

# Read line by line — memory-friendly for big files.
File.open("notes.txt", "r") do |file|
  file.each_line do |line|
    puts line.chomp.upcase    # chomp removes the trailing newline
  end
end
# -> FIRST LINE / SECOND LINE / THIRD LINE

# Useful File checks before you read:
puts File.exist?("notes.txt")    # -> true
puts File.size("notes.txt") > 0  # -> true

# A tiny real task: count non-empty lines.
count = 0
File.foreach("notes.txt") do |line|
  count += 1 unless line.strip.empty?
end
puts "Non-empty lines: #{count}"  # -> Non-empty lines: 3

Your turn. Write a to-do list to a file, then read it back numbered. Confirm the TODO details, then run it locally.

# 🎯 YOUR TURN — replace each TODO, then run with: ruby todo.rb

# 1) Write three tasks to a file, one per line
tasks = ["Buy milk", "Walk dog", "Write code"]
File.open("todo.txt", "w") do |file|     # 👉 TODO: "w" to create/overwrite
  tasks.each { |task| file.puts task }
end

# 2) Read the file back and number each task
File.readlines("todo.txt").each_with_index do |line, i|
  puts "#{i + 1}. #{line.chomp}"          # 👉 chomp drops the newline
end

# ✅ Expected output:
#    1. Buy milk
#    2. Walk dog
#    3. Write code

Mini-Challenge: A Line-Count Report

Write a file, then count its total and non-blank lines with File.foreach. Run with ruby report.rb locally.

# 🎯 MINI-CHALLENGE: a line-count report.
# Save as report.rb and run with: ruby report.rb
#
# 1. Write a few lines to "log.txt" (some blank, some with text).
# 2. Read it back with File.foreach.
# 3. Count total lines AND non-blank lines (use line.strip.empty?).
# 4. Print both counts.
#
# ✅ Example output (5 lines, 2 blank):
#    Total lines: 5
#    Non-blank: 3

# your code here

Common Errors (and the fix)

Pro Tips

📋 Quick Reference — File I/O

ConceptRuby Syntax
Write (block)File.open(f,"w") { |x| }
Quick writeFile.write(f, str)
Read wholeFile.read(f)
Read linesFile.readlines(f)
Stream linesFile.foreach(f) { }
Exists?File.exist?(f)

🎉 Lesson Complete!

Practice quiz

Why prefer the block form of File.open?

  • It is faster
  • It reads files in parallel
  • It automatically closes the file, even if an exception is raised
  • It compresses the file

Answer: It automatically closes the file, even if an exception is raised. The block form passes you the file and closes it automatically when the block ends, even on errors.

What does the "w" file mode do?

  • Opens for writing and truncates (overwrites) existing content
  • Reads the file
  • Appends to the end
  • Opens in binary mode

Answer: Opens for writing and truncates (overwrites) existing content. "w" opens for writing and empties the file first, so it overwrites; it also creates the file if missing.

Which mode appends new content without erasing what's there?

  • "r"
  • "w"
  • "x"
  • "a"

Answer: "a". "a" opens for appending, adding to the end without truncating.

What does File.read return?

  • An array of lines
  • The entire file as one string
  • One line at a time
  • The file size

Answer: The entire file as one string. File.read loads and returns the whole file as a single string.

What does File.readlines return?

  • An array with one string per line
  • One big string
  • The number of lines
  • A lazy enumerator

Answer: An array with one string per line. File.readlines returns an array, one string per line — handy for indexing or counting.

Which is the memory-efficient way to process a large file line by line?

  • File.read
  • File.readlines
  • File.foreach / each_line
  • File.size

Answer: File.foreach / each_line. File.foreach and each_line stream one line at a time, keeping memory flat regardless of file size.

What does `chomp` do to a line read from a file?

  • Uppercases it
  • Removes a trailing newline
  • Splits it into words
  • Adds a newline

Answer: Removes a trailing newline. Each read line keeps its trailing ; chomp removes it so puts doesn't double-space.

Which check confirms a file exists before reading it?

  • File.size
  • File.read
  • File.open
  • File.exist?

Answer: File.exist?. File.exist?(name) returns true/false; checking it avoids Errno::ENOENT.

Which shortcut writes a whole string to a file in one call?

  • File.put
  • File.write(name, str)
  • File.save
  • File.append

Answer: File.write(name, str). File.write("x.txt", "hi") writes the string in a single call.

What error do you get reading a file that doesn't exist?

  • ZeroDivisionError
  • ArgumentError
  • Errno::ENOENT (No such file)
  • TypeError

Answer: Errno::ENOENT (No such file). Reading a missing file raises Errno::ENOENT; check File.exist? first or create it.

Continue this course

Frequently asked questions

Why should I use the block form of File.open?

File.open("x", "w") { |f| ... } passes the open file to the block and automatically closes it when the block ends — even if an exception is raised inside. That guarantees you never leak an open file handle, which can otherwise cause data loss (writes may stay buffered until close) or 'too many open files' errors. The alternative is calling File.open without a block and remembering to call f.close yourself, usually in an ensure clause. The block form does that bookkeeping for you, so it's the recommended style.

What do the file modes 'r', 'w', and 'a' mean?

They tell Ruby how to open the file. "r" is read-only and is the default. "w" opens for writing and truncates the file to empty first — so it overwrites any existing content, and creates the file if it doesn't exist. "a" opens for appending, adding new content at the end without erasing what's there. There are also combined modes like "r+" (read and write) and a "b" suffix for binary data, but r, w, and a cover the vast majority of everyday tasks.

What's the difference between File.read, readlines, and each_line?

File.read returns the entire file as one big string — simplest, but it loads everything into memory. File.readlines returns an array with one string per line, convenient when you want to index or count lines. each_line (and File.foreach) processes the file one line at a time without loading it all, which is the memory-efficient choice for large files. The rule of thumb: read for small files you want whole, readlines when you need an array, and foreach/each_line for big files or streaming work.

Why do my lines have extra blank lines or a stray newline?

Each line read from a file usually includes its trailing newline character ( ). If you then print it with puts — which adds its own newline — you get a double blank line. The fix is chomp, which removes a trailing newline: line.chomp. Conversely, when writing, file.puts adds a newline for you, while file.print does not. So mismatched newlines almost always come from either forgetting chomp on read or mixing puts and print on write.