Skip to main content
View
Chapter 15 / 16Reading and writing files
1. Variables and data types
2. Strings and symbols
3. Arrays
4. Hashes
5. Conditionals
6. Loops and iterators
7. Methods and blocks
8. Classes and objects
9. Inheritance
10. Modules
11. Extending code with prepend
12. Enumerable
13. Where to go next
14. Error handling
15. Reading and writing files
16. Advanced class features

Reading and writing files

This chapter introduces reading and writing files, then serialization: turning Ruby objects into data that can be saved and reloaded later.

Principle

In a program, all data disappears when it terminates. To keep it, you must write it to a file on disk. On the next launch, you read it back.

Ruby provides the File class for all file operations. To save complex Ruby objects (not just text), you use serialization: Marshal (binary format) or JSON (human-readable text format).

Reading a file

The simplest way to read an entire file:

content = File.read('pokedex.txt')
puts content
  • File.read reads the entire file at once and returns a String.
  • If the file does not exist, Ruby raises an Errno::ENOENT error.

To read line by line (useful for large files):

lines = File.readlines('pokedex.txt')
lines.each { |line| puts line }
  • File.readlines returns an Array of Strings, one per line.

Writing to a file

# Write (overwrites existing content)
File.write('pokedex.txt', "Pikachu,25,electric\nCharizard,36,fire\n")

# Append to the end (without overwriting)
File.open('pokedex.txt', 'a') do |file|
file.puts 'Blastoise,40,water'
end
  • File.write creates the file if it does not exist and overwrites all existing content.
  • File.open('file', 'a') opens in append mode (a for append): new content is added at the end.

File.open with a block

File.open with a block is the safest approach: Ruby automatically closes the file at the end of the block, even if an error occurs:

File.open('pokedex.txt', 'r') do |file|
file.each_line do |line|
puts line
end
end

The main modes:

ModeMeaning
'r'Read-only (default). Error if the file doesn't exist
'w'Write. Creates the file or overwrites content
'a'Append. Creates the file or adds to the end
'r+'Read and write
  • Always prefer File.open with a block over File.open without one. Without a block, you must remember to close the file manually.

Checking if a file exists

if File.exist?('pokedex.txt')
puts 'The file exists'
else
puts 'The file does not exist'
end

puts File.directory?('pokedex') # => true if it is a directory
puts File.size('pokedex.txt') # => size in bytes
  • Always check File.exist? before reading to avoid an error.

Marshal — binary serialization

Marshal converts any Ruby object into binary data, and vice versa. It is the simplest way to save complex objects:

# Save an object
pokemon = { name: 'Pikachu', level: 25, types: [:electric] }

File.open('pikachu.dat', 'wb') do |file|
Marshal.dump(pokemon, file)
end

# Load the object
loaded = File.open('pikachu.dat', 'rb') do |file|
Marshal.load(file)
end

p loaded # => {:name=>"Pikachu", :level=>25, :types=>[:electric]}
  • Marshal.dump(object, file) writes the object to the file.
  • Marshal.load(file) reconstructs the object exactly as it was: classes, symbols, nested structures — everything is preserved.
  • 'wb' and 'rb': binary modes are mandatory for Marshal. Without the b, encoding can corrupt the data.

Marshal also works with custom classes:

class Pokemon
attr_reader :name, :level

def initialize(name, level)
@name = name
@level = level
end
end

pikachu = Pokemon.new('Pikachu', 25)

# Save
File.open('pikachu.dat', 'wb') { |file| Marshal.dump(pikachu, file) }

# Load: we get a Pokemon object directly
loaded = File.open('pikachu.dat', 'rb') { |file| Marshal.load(file) }
puts loaded.name # => Pikachu
puts loaded.level # => 25
  • After Marshal.load, you get back a Pokemon instance directly, not a Hash to convert. This is Marshal's biggest strength.

JSON — text serialization

JSON is a human-readable text format that other languages can also use. You need to load the json library:

require 'json'

# Convert a Hash to JSON
pokemon_data = { name: 'Charizard', level: 50, types: ['fire', 'flying'] }
json_string = JSON.generate(pokemon_data)
puts json_string
# => {"name":"Charizard","level":50,"types":["fire","flying"]}

# Convert the JSON back to a Hash
parsed = JSON.parse(json_string)
puts parsed['name'] # => Charizard
  • JSON.generate converts a Hash to a JSON String.
  • JSON.parse does the reverse: it returns a Hash.
  • Warning: keys become Strings after parsing. parsed[:name] returns nil, you need parsed['name'].
  • JSON does not support Symbols: :electric is converted to "electric".

To save and load from a file:

require 'json'

# Save
File.write('pokedex.json', JSON.generate(pokemon_data))

# Load
loaded_data = JSON.parse(File.read('pokedex.json'))

Marshal vs JSON

MarshalJSON
FormatBinary (unreadable)Text (readable)
Ruby typesPreserved (Symbol, classes)Lost (everything becomes String/Array/Hash)
PortabilityRuby onlyAll languages
ReconstructionAutomaticManual (you must rebuild the objects)
SecurityNever load untrusted dataSafe
  • Marshal is perfect for internal saves within a Ruby program.
  • JSON is perfect for exchanging data with other programs or for readable configuration files.

Converting objects for JSON

Since JSON does not know about Ruby classes, you need to write conversion methods:

require 'json'

class Pokemon
attr_reader :name, :level, :types

def initialize(name, level, types)
@name = name
@level = level
@types = types
end

# Convert to Hash for JSON
def to_hash
return { name: @name, level: @level, types: @types.map { |type| type.to_s } }
end

# Reconstruct from a JSON Hash
def self.from_hash(data)
types = data['types'].map { |type| type.to_sym }
return Pokemon.new(data['name'], data['level'], types)
end
end

# Save as JSON
pikachu = Pokemon.new('Pikachu', 25, [:electric])
File.write('pikachu.json', JSON.generate(pikachu.to_hash))

# Load from JSON
data = JSON.parse(File.read('pikachu.json'))
loaded = Pokemon.from_hash(data)
puts loaded.name # => Pikachu
p loaded.types # => [:electric]
  • to_hash converts Symbols to Strings (JSON does not support them).
  • self.from_hash converts Strings back to Symbols with .to_sym.
  • This is the "round-trip": object -> hash -> JSON -> hash -> object.

Conclusion

  • File.read and File.write are the simplest shortcuts. File.readlines returns an Array of lines.
  • File.open with a block automatically closes the file. Always prefer this form.
  • The modes 'r', 'w', 'a' control the type of access. 'wb'/'rb' for binary mode.
  • File.exist? checks existence before reading.
  • Marshal serializes to binary: preserves Ruby types, automatic reconstruction. Modes 'wb'/'rb' are mandatory.
  • JSON serializes to text: readable and portable, but loses Ruby types. You need to_hash and self.from_hash for the round-trip.
  • Marshal for internal saves. JSON for exchange and configuration.