Skip to content

File.open with a block closes the file for you

Level: 101 · anyone who has written with open(...) as f: in Python and wants the Ruby shape

One line: File.open(name, mode) { |f| … } closes the file when the block ends and evaluates to the block's value; Python's with open(...) closes too but is a statement with no value, counts characters where Ruby counts bytes, and translates \r\n on the way in where Ruby hands you the bytes as they are.

The block form is an ensure you do not have to write: File.open yields the open File, and whatever happens in the block — a normal end, an early return, an exception — the file is closed on the way out, and the value of the whole expression is the block's last expression. Without a block, File.open returns the open File and closing it is your job; reading it after close raises IOError. The class-method shortcuts File.read, File.write, File.readlines and File.foreach open and close for you in one call. Modes are C's — "r", "w" truncates, "a" appends, "r+" reads and writes, "b" for binary — and there is no "x": exclusive creation is spelled with the flags File::CREAT | File::EXCL. An encoding rides on the mode ("r:ISO-8859-1") or on the encoding: keyword; the bytes on disk do not change, only the label the String carries. This library runs Ruby with -E UTF-8, so the default here is UTF-8 — what it is on your machine depends on the locale, which is the text library's File.read follows the locale ↗ page. Ruby never translates line endings on Unix: a CRLF file reads back with its \r, and it is chomp that knows to strip both. The program below does all of this in a temporary directory that Dir.mktmpdir removes when its block ends; only relative names are printed.

Verified output of file_open_with_a_block_rb.rb — regenerated by tools/run_examples.py, never hand-typed.

 1. File.open with a block: closed? afterwards                 true
 2.   and the block's value is what File.open returns          :block_value
 3. File.write returns what it wrote, counted in bytes         4
 4. File.read gives the whole file as one String               "x\ny\n"
 5. File.readlines keeps each newline                          ["x\n", "y\n"]
 6. File.readlines(chomp: true) drops them                     ["x", "y"]
 7. File.foreach yields one line at a time                     "x\n" then "y\n"
 8. each_line(chomp: true) inside a block                      ["x", "y"]
 9. mode "a" appends                                           "x\ny\nz\n"
10. mode "w" truncates first                                   "fresh\n"
11. mode "r+" reads and writes in place                        "Fresh\n"
12. mode "x" (Python's exclusive create) does not exist        ArgumentError: invalid access mode x
13.   the flag form File::CREAT | File::EXCL is the same idea  Errno::EEXIST
14. File.exist? for a file that is there / not there           [true, false]
15. reading a missing file raises                              Errno::ENOENT: No such file or directory @ rb_sysopen - nope.txt
16. without a block the file stays open: closed?               false
17.   after gets and close: line, closed?                      "Fresh\n", true
18.   reading a closed file                                    IOError: closed stream
19. default encoding under -E UTF-8: read gives                "café\n" UTF-8 length=5 bytesize=6
20. encoding: "ISO-8859-1" relabels the same bytes             ISO-8859-1 length=6
21. mode "rb" gives binary (ASCII-8BIT)                        ASCII-8BIT length=6
22. File.write counts bytes, so "caf\u{e9}\n" is               6
23. CRLF file, File.read keeps the \r (no translation)         "a\r\nb\n"
24.   readlines(chomp: true) strips \r\n and \n alike          ["a", "b"]
25. the temp directory still exists inside the block           true
26. and is gone after it                                       false

Reading the output

Rows 1–2 are the claim: after the block, closed? is true, and File.open returned :block_value, the block's last expression. Rows 3–8 are the one-call class methods: File.write returns the number of bytes written, File.read returns one String, File.readlines keeps the newlines unless you pass chomp: true, and File.foreach yields a line at a time without holding the file in memory. Rows 9–13 are modes: "a" appends, "w" truncates first, "r+" lets you overwrite in place (the F replaced the f), and the "x" you may know from Python is an ArgumentError here — the flag form on row 13 is the Ruby spelling, and it raises Errno::EEXIST because the file is already there. Row 15 is what a missing file raises: Errno::ENOENT, whose message names the file. Rows 16–18 are the non-block form: the file stays open until you close it, and a read afterwards is IOError: closed stream. Rows 19–22 are encodings: the same six bytes are five characters as UTF-8, six as ISO-8859-1 and six as binary; File.write counts bytes, so writing "café\n" returns 6. Rows 23–24: File.read keeps the \r of a CRLF file, and readlines(chomp: true) strips \r\n and \n alike. Rows 25–26: the temporary directory exists inside Dir.mktmpdir's block and is gone after it — the same block-closes-it shape as File.open.

Compared with Python

Python's with open(name, mode) as f: is the same guarantee — f.closed is True afterwards (row 1) — but with is a statement, so there is no row 2: nothing is returned. Path.write_text returns a count of characters, not bytes (rows 3 and 22 differ: 4 is the same for ASCII, but "café\n" is 5 characters and 6 bytes). Python has the "x" mode Ruby lacks (row 12, FileExistsError), and its missing-file error is FileNotFoundError (row 15) — both subclasses of OSError, where Ruby's Errno::ENOENT and Errno::EEXIST are subclasses of SystemCallError. A read after close() is a ValueError, not an IOError (row 18). Row 19 is the deepest difference: open() in text mode decodes and gives you a str, and "rb" gives you bytes, a different type (row 21) — Ruby gives a String in every mode and changes its encoding label. The default text encoding is the locale's unless UTF-8 mode is on; here it is on because the examples run under the C locale, which is why the row prints utf8_mode=1 (the Python library's Opening a file ↗ page measures the locale default). Rows 23–24 are the newline difference: Python's text mode translates \r\n to \n on read (universal newlines) unless you pass newline="", while Ruby hands you the \r — see What ends a line ↗ for Python's side and chomp knows about CRLF ↗ for Ruby's.

Verified output of file_open_with_a_block_py.py — regenerated by tools/run_examples.py, never hand-typed.

 1. with open() as f: f.closed afterwards                      True
 2.   a with statement has no value (it is a statement)        n/a
 3. Path.write_text returns what it wrote, in characters       4
 4. Path.read_text gives the whole file as one str             'x\ny\n'
 5. f.readlines() keeps each newline                           ['x\n', 'y\n']
 6. read().splitlines() drops them                             ['x', 'y']
 7. for line in f yields one line at a time                    'x\n' then 'y\n'
 8. [line.rstrip('\n') for line in f] inside the with          ['x', 'y']
 9. mode "a" appends                                           'x\ny\nz\n'
10. mode "w" truncates first                                   'fresh\n'
11. mode "r+" reads and writes in place                        'Fresh\n'
12. mode "x" (exclusive create) exists and raises              FileExistsError
13.   os.O_CREAT | os.O_EXCL is the flag form of the same idea os.open
14. os.path.exists for a file that is there / not there        [True, False]
15. reading a missing file raises                              FileNotFoundError: No such file or directory - nope.txt
16. without a with the file stays open: closed                 False
17.   after readline and close: line, closed                   'Fresh\n', True
18.   reading a closed file                                    ValueError
19. default encoding (UTF-8 mode via the C locale): read gives 'café\n' utf-8 len=5 bytes=6 utf8_mode=1
20. encoding="latin-1" decodes the same bytes differently      latin-1 len=6
21. mode "rb" gives bytes, a different type                    bytes len=6
22. write_text counts characters, so 'caf\N{...}\n' is         5
23. CRLF file, text mode translates \r\n to \n                 'a\nb\n'
24.   newline='' keeps the \r; splitlines() strips both        'a\r\nb\n' ['a', 'b']
25. the temp directory still exists inside the with            True
26. and is gone after it                                       False

Try it

  1. Write wc(name) returning the line, word and byte counts of a file, reading it with File.foreach so it is never held in memory whole. Check it on an empty file and on a file whose last line has no newline.
  2. Write append_unless_present(name, line) that appends line only when the file does not already contain it, and prove it is idempotent: call it twice and print File.readlines(name, chomp: true).
Solution to 1

file_open_with_a_block_kata_rb.rb in full — pasted here by tools/run_examples.py from the file CI runs.

# Kata: a small `wc` -- lines, words and bytes of a file -- written with
# File.foreach so the file is never held in memory whole. The file is created in
# a temporary directory and only its relative name is printed.
require "tmpdir"

def wc(name)
  lines = words = 0
  File.foreach(name) do |line|
    lines += 1
    words += line.split.size
  end
  [lines, words, File.size(name)]
end

Dir.mktmpdir do |dir|
  Dir.chdir(dir) do
    File.write("poem.txt", "the quick brown fox\njumps over\nthe lazy dog\n")
    lines, words, bytes = wc("poem.txt")
    puts format("%4d %4d %4d %s", lines, words, bytes, "poem.txt")
    File.write("empty.txt", "")
    puts format("%4d %4d %4d %s", *wc("empty.txt"), "empty.txt")
    File.write("nonl.txt", "no newline at the end")
    puts format("%4d %4d %4d %s", *wc("nonl.txt"), "nonl.txt")
  end
end

Verified output of file_open_with_a_block_kata_rb.rb — regenerated by tools/run_examples.py, never hand-typed.

   3    9   44 poem.txt
   0    0    0 empty.txt
   1    5   21 nonl.txt

See also