I was experimenting with face definitions and created a number of "test" faces that were meant to be temporary. At the end of the experiments, I wanted to clean up after myself, so I attempted (unintern FACE), but the faces remained on the return value of function (face-list). My guess was that the faces are stored in a different obarray. Then I looked at the function definition for (face-list) in file faces.el, and it gets its data from variable face-new-frame-defaults which is defined in C source code, which is a simple list, and not seemingly updated.

As a complication to testing and debugging, the doc-string for unintern indicates that FACE can be specified either as a string or as a symbol. However, the function behaves differently for the alternatives. This is kind of a tangent, and may be a bug in unintern, but it confused debugging my situation. For a normal symbol (setq my-temp-test-symbol "HELP!"), performing unintern with a string argument correctly returned t on symbol-found-and-uninterned and nil on symbol-not-found; However, when passed a symbol 'my-temp-test-symbol, it returned t repeatedly.

For faces, performing unintern with a symbol argument always returns t, and with a string argument does first return t, and then nil, but the face continues to appear in the output of (face-list).

  • 1
    (unintern 'mysym) always returns t since the reader sees 'mysym interns it and only afterwards unintern is called with the (newly interned) symbol as argument. Opposed to that (progn (setq symholder 'mysym) (list (unintern symholder) (unintern symholder))) returns (t nil) since the reader does not read mysym anymore after the setq but only symholder. The second example shows that you can pass a symbol without interning it as value of another symbol. See Creating and Interning Symbols.
    – Tobias
    Commented Mar 19, 2019 at 11:38
  • 3
    Goodness, you don't want to unintern a symbol if the face properties are the only things you are interested in. A symbol might have a variable value and a function value and who knows what other properties besides being a face. I mean, for my-temp-test-symbol then sure, go ahead -- that one is obviously yours to start with -- but in general? Definitely not the way to go.
    – phils
    Commented Mar 19, 2019 at 12:26
  • 2
    I strongly recommend to stay very far away from unintern unless you really know what it does. It's a great footgun, tho.
    – Stefan
    Commented Mar 19, 2019 at 16:56

2 Answers 2


I think you cannot really delete the faces from the internal face table of the current frame. Maybe I am mistaken in that point because I did not carefully inspect the c-source code. So I would very much appreciate acknowledgement of somebody who knows the Emacs internals better.

Gilles is right in his answer that you can change the default faces for newly created frames.

I did a little experiment in the scratch buffer. The results are given as comments.

(defface myface '((t :inverse-video t))
  "Doc" :group 'test)
;; => myface

(insert (propertize "test" 'font-lock-face 'myface))
;; => inverted "test" inserted into the buffer

(get 'myface 'face)
;; => 773

(setq face-new-frame-defaults (cl-delete-if (lambda (face) (string-equal (car face) "myface")) face-new-frame-defaults))
;; => new list of faces

(get 'myface 'face)
;; => 773

(unintern 'myface)
;; t

(defface myface '((t :inverse-video t))
  "Doc" :group 'test)
;; => myface

(insert (propertize "test" 'font-lock-face 'myface))
;; => inverted "test" inserted into the buffer

(get 'myface 'face)
;; => 774

As one sees. After the first (get 'myface 'face) one gets 773 and after the second -- inspit of the attempts to delete the face -- one gets 774.

Another experiment: The following sequence of instruction gives you a new frame with only shades of gray. The old frame remains colorful. Also new input in the old frame lets font-lock colorize the text as before in the old frame. On the other hand the new frame spies error messages about missing fonts into the message buffer.

(setq old-faces face-new-frame-defaults)
(setq face-new-frame-defaults nil)

Note, also the list returned by (frame-face-alist) is a list newly created from the internal face table. So destructively deleting an entry from that list does also not delete the entry from the internal face table.


Faces are not held in an obarray. unintern won't help you to delete a face.

There's no way to completely delete a face. The best you can do is to “hide” it by removing it from the list of faces that facep tests and face-list enumerates. As you've already found, what defines a face in this respect is being present in face-new-frame-defaults.

There is no primitive function to remove a frame from face-new-frame-defaults, but you can make one. Note that I haven't fully checked for side effects, I just had a quick look at the code in xfaces.c and didn't see anything obviously wrong with doing this.

(defun delete-face (face)
  "Delete a face. FACE can be a string or symbol.
After calling this function, `(facep FACE)' will return nil.
Note that the face should not be in use anywhere."
  (interactive (list (completing-read
              "Delete face: " face-new-frame-defaults
              nil t nil 'face-name-history)))
  (if (stringp face)
      (setq face (intern face)))
  (setq face-new-frame-defaults (assq-delete-all face face-new-frame-defaults))
  (mapc (lambda (frame)
      (let ((alist (frame-face-alist frame)))
        (assq-delete-all face alist)
        ;; There's no way to set a frame's face alist, so there's no way to
        ;; remove the first element in all circumstances.
        (when (eq (caar alist) face)
          (if (null (cdr alist))
          (setcar alist (cons nil nil))
        (setcar alist (cadr alist))
        (setcdr alist (cddr alist))))))
  (cl-remf (symbol-plist face) 'face))

Note that I haven't really tested this, so I don't know if bad things may happen if the face is still in use.

This doesn't remove all resources associated with the face. For example, faces have a unique identifier which is recorded in an internal array (the face numerical identifier which is stored in the symbol's face property), and there's no way to remove an entry from this internal array.

Regarding unintern, if you run (unintern 'my-temp-test-symbol), it always returns t. This is correct behavior: my-temp-test-symbol exists when you pass it to unintern. If you already have an interned symbol, then by definition it exists. If you want to see unintern return nil, you have to pass it something that isn't an interned symbol. It can be a symbol in a different obarray, an uninterned symbol, or a string.

    (let ((sym (make-symbol)))
      (unintern sym))
    (let* ((sym (intern (symbol-name (gensym))))
           (first (unintern sym))
           (second (unintern sym)))
      (list first second))
    (let* ((vec (make-vector 7 0))
           (sym (intern "foo" vec))
           (first (unintern sym vec))
           (second (unintern sym)))
      (list first second))
  • Hm, (get 'myface 'face) returns an id. That property does not change when one deletes the face from face-new-frame-defaults, does it? What does this id indicate? The position of the face in the frame face list? face-new-frame-defaults is only for new frames, isn't it? I experimented a bit in the scratch buffer.
    – Tobias
    Commented Mar 19, 2019 at 14:01
  • @Tobias The id is an index in an internal face array. There's no Lisp function that allows removing entries from this array. And indeed face-new-frame-defaults applies to new frames, but it's a bit more than that: it's also the “user interface” to faces. So removing a face from there does effectively remove the face from the user interface, even if it doesn't completely remove all traces of the face. Commented Mar 19, 2019 at 15:09
  • I am getting following error: Symbol’s value as variable is void: face-new-frame-defaults
    – alper
    Commented May 10, 2022 at 22:54
  • @alper In Emacs 28 (github.com/emacs-mirror/emacs/commit/…) it changed to face--new-frame-defaults with a different format. Commented May 11, 2022 at 9:31

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.