Read and write files in Emacs Lisp

Apparently it is not so uncommon to use interactive commands like find-file or write-file to read or write files in Emacs Lisp. Small libraries do it, Org Mode is no exception, even built-in libraries use this pattern 1. Which is a good example that we should scarcely follow Emacs’ own code as in fact it is not a good idea to use these commands from Emacs Lisp.

User-Visible Effects?

When you are writing internal state to disk you should not interfere with the user but interactive commands like find-file or write-file are meant to have user-visible effects—and they do have user-visible effects even when used in non-interactive Emacs Lisp. These effects—while desirable in interactive use—will confuse users and interrupt their workflow if they show up unexpectedly.

write-file for instance sets the major mode of the current buffer which in turn runs the major mode hook—one of the central extension points in Emacs. Writing to a file can thus end up running arbitrary code from the user’s configuration! Another example: find-file tries to set local variables for the current buffer which can prompt the user if a value is unsafe or a variable is risky—imagine if you would see an unexpected prompt about a local variable in a buffer that you did not even create yourself.

Libraries To The Rescue

The best way for non-interactive IO is f.el by Johan Andersson of Cask fame. With this library reading and writing files is easy enough with f-read-text and f-write-text respectively:

(let* ((filename (locate-user-emacs-file "foo.txt"))
       (contents (f-read-text filename 'utf-8)))
  (f-write-text (upcase contents) 'utf-8 filename))

f.el also provides f-read-bytes and f-write-bytes for reading and writing raw bytes. And while you are at it take a look at the rest of f.el—there are a couple of nice functions in there for dealing with paths and files in Emacs Lisp.

The Tedious Way

That said f.el can do no magic; it uses the same Emacs Lisp primitives that are also available to you. If you cannot use external libraries—you may be contributing to a GNU ELPA package which outlaws non-gnu dependencies?—you can still read files properly, albeit with a little more boilerplate. Use insert-file-contents-literally, which avoids almost every side effect that reading files has in Emacs Lisp2, to insert the raw contents of the file into a temporary buffer and then decode the raw bytes with decode-coding-region3:

  (insert-file-contents-literally file-name)
  (decode-coding-region (point-min) (point-max) 'utf-8 t))

The inverse direction is a little more involved. No “write” function like insert-file-contents-literally exists so you need to be a little more explicitly to prevent Emacs from making guesses. Bind coding-system-for-write to force Emacs to write binary data and then insert the utf-8-encoded data into a special temporary buffer which automatically gets written to the destination path at the end of the form45:

(let ((coding-system-for-write 'binary))
  (with-temp-file path
    (set-buffer-multibyte nil)
    (encode-coding-string contents utf-8 nil (current-buffer))))


Do not use find-file, write-file or any other interactive command to read or write files from Emacs Lisp, to avoid unintended side-effects like mode hooks or even prompts about local variables. Use f-read-text and f-write-text from the f.el library instead, or write your own safe functions as shown above.

  1. I’m linking to the GitHub mirror of the Emacs sources because the UI is noticeably faster. When I looked for the link to the specific source location I couldn’t help but loose a few minutes on the list of pull requests. One would assume that it’s a well-known fact that Emacs isn’t hosted on GitHub, and the GitHub page even says that it’s a mirror, but people are so eager to contribute that they ignore that. Now, of course, the PRs are trivial, but still… imagine the potential contributors lost here, imagine where Emacs was if it would welcome its contributors.

  2. I think even insert-file-contents-literally still runs find-file-hook but I’m just too lazy to find out now According to a reader it does not even run find-file-hook.

  3. The arguments to decode-coding-region are self-explanatory—except the last one. Why pass t? And t literally, not following the good practice of passing named symbols for non-nil arguments? I dearly recommend to look up this parameter in the docstring: It’s a nice lesson about flawed API design and the pain of programming Emacs Lisp.

  4. with-temp-file is an unfortunate and confusing name. It doesn’t refer to temporary files at all, but instead to a temporary buffer that gets written to a file. Emacs Lisp can be confusing at whiles.

  5. Thanks to the reader who caught a mistake in this code example. ↩︎