skip to content

Files & I/O

Reading and writing files with open modes and block form, walking directories with Pathname, the standard streams, and JSON, CSV and YAML. Interviewers check you close handles and load data safely.

on this pageshow

explore

questions

22

In Ruby, why is File.open with a block preferred over opening and closing the file yourself, and what does the block form return?

level: juniorimportance: must knowfreq 72%

answer

  1. who owns the handle
  2. closes on normal exit and on raise
  3. returns the block's value
  4. no block: returns the File
  5. Errno::EMFILE, Too many open files

basics

~20 s

File.open with a block yields the File, closes it when the block exits, even through an exception, and returns the block's value. Without a block it returns the File, and the caller must close it or leak the descriptor.

solid answer

~40 s

`File.open(path) { |f| ... }` yields the open `File`, closes it in an ensure-style cleanup whether the block returns normally, `break`s or raises, and then returns whatever the block returned. Without a block it returns the `File` object and the caller owns it: an exception between the open and the `close` skips the close, and the descriptor stays open until the garbage collector happens to reclaim the object. In a long-running process that leaks descriptors until the per-process limit is reached and `File.open` raises `Errno::EMFILE` (Too many open files). So the block form is the default; the no-block form is for a handle that must outlive one method, paired with `begin ... ensure f.close end`.

code

ruby · 20 lines
ruby
# Block form: returns the block's value; the file is already closed
first_line = File.open("app.log") { |f| f.gets }

handle = nil
begin
  File.open("app.log") do |f|
    handle = f
    raise ArgumentError, "bad record"
  end
rescue ArgumentError
  handle.closed? # => true, closed before the error reached us
end

# No block: the caller owns the File and must close it
f = File.open("app.log")
begin
  header = f.gets
ensure
  f.close
end

go deeper

for a junior

Recall that the block form closes the file for you on every exit and returns the block's value; reach for it by default.

for a middle

Explain that the block form is an ensure around the block, why an exception or early return leaks a no-block handle, and what IOError you get from a closed File.

for a senior

Connect leaked handles to Errno::EMFILE in long-running workers, know that GC finalization closes files only eventually, and show the begin/ensure pattern for handles that outlive a method.

for a principal

Frame resource ownership as an API design rule: expose block-taking methods that own cleanup, so callers of your library cannot leak descriptors by forgetting a close.

## Two ways to call File.open Ruby's `File.open(path, mode = "r")` has two shapes, and they differ in **who is responsible for closing the file**. - **Block form** — `File.open("app.log") { |f| f.gets }`. Ruby opens the file, yields the `File` object to the block, and closes it when the block finishes. The method returns **the block's value**, not the file. - **No-block form** — `f = File.open("app.log")`. Ruby returns the open `File` object. The caller owns it and must call `f.close`. The rdoc for `File.open` in `io.c` says exactly this: with no block it returns the File object; with a block it calls the block with the File and returns the block's value. In the C source the block form is implemented as `rb_ensure(rb_yield, io, io_close, io)` — the close is registered as an *ensure* step, so it runs however the block exits. ## What the block form guarantees 1. The file is closed when the block returns normally. 2. The file is closed when the block raises; the exception then propagates to the caller unchanged. 3. The file is closed when the block exits early through `break`, `next` or a `return` from the enclosing method. 4. `File.open` returns the block's last value, so `data = File.open(path) { |f| f.read }` hands back the contents with no open handle left behind. The `File` object itself survives the block if you smuggle it out, but it is closed: `f.closed?` returns `true`, and reading from it raises `IOError` (closed stream). ## What goes wrong with the no-block form The no-block form is not wrong, but it makes cleanup your job, and the usual mistakes are: - **An exception between open and close.** `f = File.open(p); parse(f.read); f.close` never reaches `close` if `parse` raises. - **Early returns.** A guard clause that returns before `close` leaves the handle open. - **Relying on the garbage collector.** Ruby does close a stream when the GC reclaims its `File` object, but *when* that happens is not under your control. Until then the descriptor stays open, and buffered writes may not have reached the file. - **Descriptor exhaustion.** Every process has a limit on open file descriptors. A worker that opens files in a loop without closing them eventually fails in `File.open` with `Errno::EMFILE` — the kernel's "Too many open files" error — often far from the code that leaked. When a handle really must outlive a single method (a log writer kept open for the life of an object, say), write the cleanup explicitly: ```ruby f = File.open("export.csv", "w") begin rows.each { |row| f.puts(row.join(",")) } ensure f.close end ``` ## Block form and the one-shot class methods For whole-file work you often do not need a handle at all. `File.read(path)`, `File.write(path, data)`, `File.readlines(path)` and `File.foreach(path) { ... }` each open the file, do their work and close it before returning. They are shorthand for the block form; choosing between them is about memory and access pattern, not about closing. | Call | Returns | Who closes | |---|---|---| | `File.open(path) { \|f\| ... }` | the block's value | Ruby, on any exit | | `File.open(path)` | the open `File` | you, with `close` | | `File.read(path)` | the contents as a `String` | Ruby, before returning | | `File.write(path, data)` | bytes written (`Integer`) | Ruby, before returning | ## Interview traps - Saying the block form returns the file. It returns the block's value. - Saying the file stays open if the block raises. The close runs first, then the exception continues. - Saying Ruby closes a file "when the variable goes out of scope". Ruby has no scope-based destruction; an explicit `close`, the block form, eventual garbage collection or process exit is what closes it. - Forgetting that a closed `File` object still exists — using it afterwards raises `IOError`, not `NoMethodError`. A good answer names the guarantee (closed on every exit path), the return value (the block's), and the failure it prevents (`Errno::EMFILE` in a long-running process).

  • What happens if you keep the File yielded by File.open and call read on it after the block ends?
    The Ruby object still exists, but its descriptor was closed when the block finished, so `f.closed?` is `true` and `f.read` raises `IOError` (closed stream). If the data is needed later, return it from the block, as in `data = File.open(path) { |f| f.read }`, instead of leaking the handle out.
  • Is a File you forgot to close ever closed at all?
    Yes, eventually: Ruby closes a stream when the garbage collector reclaims its `File` object. You do not control when that happens, so a loop that opens files faster than objects are collected can still hit the descriptor limit and raise `Errno::EMFILE`, and buffered writes may sit unflushed until the object is finalized. It is a delayed leak, not cleanup.
  • Do File.read, File.write and File.foreach need a block or an explicit close?
    No. Each opens the path, does its work and closes the stream before returning; `File.foreach` with a block closes when iteration ends or the block raises. Without a block, `File.foreach` returns an `Enumerator` that only opens the file when something iterates it.

The block form is a hotel key card that stops working at checkout, whether you leave calmly or run out during a fire alarm. The no-block form is a metal key you must remember to hand back; if you forget, the hotel only notices much later, and meanwhile the room stays taken.

saying these in an interview costs you the question

  • The block form only closes the file if the block finishes without raising.
  • File.open with a block returns the File object so you can keep reading it later.
  • Ruby closes a file as soon as the variable holding it goes out of scope.
  • You never need close because the garbage collector cleans up files promptly.
  • File.read leaves the file open until you close it yourself.
open as a page

In Ruby, what does JSON.parse return for a JSON object, and why does data[:name] come back nil unless you pass symbolize_names: true?

level: juniorimportance: must knowfreq 68%

basics

~20 s

JSON.parse turns a JSON object into a Hash whose keys are Strings, arrays into Arrays and null into nil. data[:name] looks up a Symbol key that does not exist, so it returns nil; symbolize_names: true makes the keys Symbols.

open as a page

In Ruby, why does a relative path like "config/settings.yml" break when a script runs from another directory, and how do __dir__ and File.expand_path fix it?

level: juniorimportance: must knowfreq 60%

basics

~10 s

Relative paths resolve against the process's current working directory, not the source file's location. dir returns the absolute directory of the current file, so File.expand_path("config/settings.yml", dir) or File.join(dir, "config", "settings.yml") works from anywhere.

open as a page

In Ruby, what is the difference between $stdout and STDOUT, and which one should you reassign to redirect output?

level: juniorimportance: must knowfreq 58%

basics

~10 s

$stdout is a global variable holding the current standard output stream, and Kernel#puts writes to it; STDOUT is a constant holding the original stream. Redirect by reassigning $stdout; STDOUT stays as the way back.

open as a page

In Ruby, how would you use Dir.glob to find old image thumbnails in nested directories, and what does it skip or sort by default?

level: middleimportance: must knowfreq 50%

basics

~10 s

Dir.glob("uploads/**/*_thumb.{jpg,png}") walks every subdirectory and returns matching paths, sorted by default since Ruby 3.0. It skips hidden entries unless File::FNM_DOTMATCH is passed, and returns an empty array when nothing matches.

open as a page

In Ruby, how do you capture what a command-line tool prints to $stdout and $stderr inside a test, and what output escapes that capture?

level: middleimportance: must knowfreq 50%

basics

~20 s

Swap $stdout and $stderr for StringIO objects, run the tool, read each buffer's string, and restore the originals in ensure. Child processes, writes to STDOUT or STDERR, and objects that cached the old stream escape the swap.

open as a page

A Ruby job calls File.readlines on a 5 GB access log and gets killed for memory; why, and how do you rewrite it?

level: seniorimportance: must knowfreq 55%

basics

~20 s

File.readlines reads the whole file into an Array with one String per line, so a 5 GB log needs more than 5 GB of heap. File.foreach, or each_line on an open File, yields one line at a time and keeps memory flat.

open as a page

In Ruby 4.0, how do YAML.load, YAML.safe_load and YAML.unsafe_load differ, and why does loading a config containing a date raise Psych::DisallowedClass?

level: seniorimportance: must knowfreq 55%

basics

~20 s

Since Psych 4 (Ruby 3.1), YAML.load is safe_load that also permits Symbol: only plain scalars, arrays and hashes load, and aliases are off. A date value would build a Date, which is not permitted, so pass permitted_classes: [Date]. unsafe_load builds any tagged class.

open as a page

In Ruby's File.open, what do the modes r, w, a and r+ do, and what does adding b to a mode change?

level: juniorimportance: should knowfreq 58%

basics

~20 s

Mode r, the default, reads an existing file; w creates or truncates and writes; a creates if needed and always writes at the end; r+ reads and writes an existing file without truncating. Adding b makes the stream binary.

open as a page

In Ruby, why can code that checks File.exist? before calling File.open still raise Errno::ENOENT, and what should it do instead?

level: middleimportance: should knowfreq 35%

basics

~20 s

File.exist? and File.open are two separate system calls, and another process can delete or move the file between them. Open directly and rescue Errno::ENOENT; to create only if absent, open with wx and rescue Errno::EEXIST.

open as a page

In Ruby, what does IO#gets return at the end of a file, and what does its chomp: true keyword change?

level: middleimportance: should knowfreq 45%

basics

~20 s

IO#gets returns the next line including its trailing newline, and nil at end of file, which is why while (line = f.gets) terminates. With chomp: true the line separator, including a preceding carriage return, is removed.

open as a page

In Ruby 4.0, what does CSV.foreach(path, headers: true) yield for each line, and what do the converters and header_converters options change?

level: middleimportance: should knowfreq 45%

basics

~20 s

With headers: true, CSV.foreach yields one CSV::Row per data line, indexed by header name, with every field a String. converters: :numeric turns numeric-looking fields into Integer or Float; header_converters: :symbol turns headers into snake_case Symbols.

open as a page

In Ruby, how do JSON.generate, JSON.pretty_generate and to_json differ, and what happens when you serialize an object of your own class?

level: middleimportance: should knowfreq 42%

basics

~10 s

JSON.generate returns compact JSON, JSON.pretty_generate the same with two-space indentation, and to_json is the per-object hook the generator calls. An object without its own to_json is silently written as its to_s string.

open as a page

In Ruby 4.0, whose default json gem is 2.18, how does JSON.load differ from JSON.parse, and which belongs on untrusted input?

level: middleimportance: should knowfreq 32%

basics

~20 s

JSON.load is a lenient loader for trusted data: it reads IOs, maps empty input to nil, accepts NaN and, in json 2.18, builds any class a json_class key names that defines json_create. Untrusted input belongs in JSON.parse.

open as a page

In Ruby's FileUtils, how do mkdir_p, cp, mv and rm_rf behave, and why is rm_rf riskier than rm_r?

level: middleimportance: should knowfreq 40%

basics

~20 s

FileUtils.mkdir_p creates missing parents and ignores an existing directory; cp copies files but not directories (cp_r does); mv renames, copying across file systems. rm_rf is rm_r with force: true, which swallows errors, so a wrong path fails silently.

open as a page

In Ruby 4.0, what does Pathname's / operator do, and why does Pathname("uploads") / "/etc/hosts" differ from File.join("uploads", "/etc/hosts")?

level: middleimportance: should knowfreq 35%

basics

~20 s

Pathname#/ is an alias of Pathname#+: it appends a relative fragment and returns a new Pathname, but an absolute right-hand side replaces the left entirely. So Pathname("uploads") / "/etc/hosts" is /etc/hosts, while File.join gives "uploads/etc/hosts".

open as a page

In Ruby, why does a script run as `ruby greet.rb Alice` fail with Errno::ENOENT when it calls gets to read a reply?

level: middleimportance: should knowfreq 36%

basics

~20 s

Kernel#gets reads from ARGF, which treats every entry in ARGV as a file to read and uses $stdin only when ARGV starts out empty. With Alice in ARGV, gets tries to open a file named Alice. Call $stdin.gets instead.

open as a page

In Ruby, what is StringIO, and why does reading from a StringIO right after writing to it return an empty string?

level: middleimportance: should knowfreq 40%

basics

~20 s

StringIO, from the stringio default gem, wraps a String in IO-like methods such as puts, gets and read. Reads and writes share one position, so after a write it sits at the end; call rewind first, or read #string.

open as a page

In Ruby, what can Marshal.dump not serialize, what must exist before Marshal.load can rebuild an object, and when is Marshal the wrong choice?

level: seniorimportance: should knowfreq 28%

basics

~20 s

Marshal.dump refuses procs, methods, IO objects, anonymous classes, objects with singleton methods and hashes with a default proc, raising TypeError. Marshal.load needs every class defined, or raises ArgumentError. It is wrong for untrusted bytes and for data read by other programs.

open as a page

In Ruby, why can a long-running service using Tempfile.new fill the temp directory, and how do Tempfile.create and Dir.mktmpdir make cleanup predictable?

level: seniorimportance: should knowfreq 30%

basics

~20 s

Unless you unlink it, Tempfile.new deletes its file only when the Tempfile is garbage collected or the process exits normally, so files pile up. Tempfile.create with a block and Dir.mktmpdir with a block delete as soon as the block ends; anonymous: true removes the name immediately.

open as a page

A Ruby worker's puts lines reach the container log late and out of order with its warn lines, though the terminal looked fine; why, and how do you fix it?

level: seniorimportance: should knowfreq 38%

basics

~20 s

When standard output is a pipe or file, Ruby buffers STDOUT internally, while STDERR is always synchronous, so warn lines overtake puts lines. Set $stdout.sync = true at boot, or call $stdout.flush where output must appear.

open as a page

In Ruby, what does IO.pipe return, and why can a read on its reader block forever after the writer has sent everything?

level: seniorimportance: nice to knowfreq 18%

basics

~20 s

IO.pipe returns [reader, writer], two IO objects joined by an OS pipe. The reader sees end of file only when every copy of the write end is closed, so reader.read hangs while any writer, including one inherited by fork, stays open.

open as a page