Emacs Lisp package development standards and conventions
Provides Emacs Lisp package development standards and conventions. Use when creating or reviewing .el packages to ensure proper headers, naming, metadata, and MELPA compliance.
/plugin marketplace add hugoduncan/library-skills/plugin install emacs-libraries@library-skillsThis skill inherits all available tools. When active, it can use any tool Claude has access to.
Comprehensive guide to Emacs Lisp package development standards, covering naming, structure, metadata, and distribution requirements.
Emacs packages follow strict conventions to ensure compatibility, discoverability, and quality. These conventions cover file structure, naming, metadata, documentation, and distribution through package archives like MELPA and GNU ELPA.
Single .el file with header metadata.
;;; mypackage.el --- Brief description -*- lexical-binding: t; -*-
;; Copyright (C) 2025 Your Name
;; Author: Your Name <you@example.com>
;; Version: 1.0.0
;; Package-Requires: ((emacs "25.1"))
;; Keywords: convenience, tools
;; URL: https://github.com/user/mypackage
;;; Commentary:
;; Longer description of what the package does.
;;; Code:
(defun mypackage-hello ()
"Say hello."
(interactive)
(message "Hello from mypackage!"))
(provide 'mypackage)
;;; mypackage.el ends here
Directory with -pkg.el descriptor file.
Structure:
mypackage/
├── mypackage.el
├── mypackage-utils.el
├── mypackage-pkg.el
└── README.md
mypackage-pkg.el:
(define-package "mypackage" "1.0.0"
"Brief description"
'((emacs "25.1")
(dash "2.19.1"))
:keywords '("convenience" "tools")
:url "https://github.com/user/mypackage")
Simple package (single .el):
;;; filename.el --- description;; Author:;; Version: or ;; Package-Version:;;; Commentary:;;; Code:(provide 'feature-name);;; filename.el ends here;;; mypackage.el --- Brief one-line description -*- lexical-binding: t; -*-
;; Copyright (C) 2025 Author Name
;; Author: Author Name <email@example.com>
;; Maintainer: Maintainer Name <email@example.com>
;; Version: 1.0.0
;; Package-Requires: ((emacs "25.1") (dash "2.19.1"))
;; Keywords: convenience tools
;; URL: https://github.com/user/mypackage
;; SPDX-License-Identifier: GPL-3.0-or-later
;;; License:
;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;;; Commentary:
;; Detailed description spanning multiple lines.
;; Explain what the package does, how to use it.
;;; Code:
Always enable lexical binding:
;;; mypackage.el --- Description -*- lexical-binding: t; -*-
Required for modern Emacs development and MELPA acceptance.
Choose short, unique prefix. All public symbols must use this prefix.
;; Package: super-mode
;; Prefix: super-
(defun super-activate () ; ✓ Public function
...)
(defvar super-default-value nil) ; ✓ Public variable
(defun super--internal-helper () ; ✓ Private function (double dash)
...)
(defvar super--state nil) ; ✓ Private variable
Public vs Private:
prefix-nameprefix--name (double dash)Variable types:
prefix-hook-functionprefix-mode-hookprefix-enable-featureprefix--internal-stateSpecial cases:
list-frobs in frob packageprefix-modeprefix-minor-modeUse lowercase with hyphens (lisp-case):
(defun my-package-do-something () ; ✓
...)
(defun myPackageDoSomething () ; ✗ Wrong
...)
Semantic versioning: MAJOR.MINOR.PATCH
;; Version: 1.2.3
For snapshot builds:
;; Package-Version: 1.2.3-snapshot
;; Version: 1.2.3
Specify minimum Emacs version and package dependencies:
;; Package-Requires: ((emacs "26.1") (dash "2.19.1") (s "1.12.0"))
Each dependency: (package-name "version")
Use standard keywords from finder-known-keywords:
;; Keywords: convenience tools matching
Common keywords:
convenience - Convenience featurestools - Programming toolsextensions - Emacs extensionslanguages - Language supportcomm - Communicationfiles - File handlingdata - Data structuresCheck available: M-x describe-variable RET finder-known-keywords
Always end with provide:
(provide 'mypackage)
;;; mypackage.el ends here
Feature name must match file name (without .el).
Don't modify Emacs on load:
;; ✗ Bad - changes behavior on load
(global-set-key (kbd "C-c m") #'my-command)
;; ✓ Good - user explicitly enables
(defun my-mode-setup ()
"Set up keybindings for my-mode."
(local-set-key (kbd "C-c m") #'my-command))
Mark interactive commands for autoloading:
;;;###autoload
(defun my-package-start ()
"Start my-package."
(interactive)
...)
;;;###autoload
(define-minor-mode my-mode
"Toggle My Mode."
...)
Define customization group:
(defgroup my-package nil
"Settings for my-package."
:group 'applications
:prefix "my-package-")
(defcustom my-package-option t
"Description of option."
:type 'boolean
:group 'my-package)
Functions:
(defun my-package-process (input &optional format)
"Process INPUT according to FORMAT.
INPUT should be a string or buffer.
FORMAT, if non-nil, specifies output format (symbol).
Return processed result as string."
...)
First line: brief description ending with period. Following lines: detailed explanation. Document arguments in CAPS. Document return value.
Variables:
(defvar my-package-cache nil
"Cache for processed results.
Each entry is (KEY . VALUE) where KEY is input and VALUE is result.")
User options:
(defcustom my-package-auto-save t
"Non-nil means automatically save results.
When enabled, results are saved to `my-package-save-file'."
:type 'boolean
:group 'my-package)
Verify documentation:
M-x checkdoc RET
Requirements:
package-lint:
M-x package-lint-current-buffer
Checks:
flycheck-package:
(require 'flycheck-package)
(flycheck-package-setup)
Real-time package.el validation.
Missing lexical binding:
;;; package.el --- Description -*- lexical-binding: t; -*-
Wrong provide:
;; File: my-utils.el
(provide 'my-utils) ; ✓ Matches filename
;; File: my-package.el
(provide 'my-pkg) ; ✗ Doesn't match filename
Namespace pollution:
;; ✗ Bad
(defun format-string (s) ; Collides with other packages
...)
;; ✓ Good
(defun my-package-format-string (s)
...)
Global state on load:
;; ✗ Bad
(setq some-global-var t) ; Changes Emacs on load
;; ✓ Good
(defcustom my-package-feature-enabled nil
"Enable my-package feature."
:type 'boolean
:set (lambda (sym val)
(set-default sym val)
(when val (my-package-activate))))
Source control:
Structure:
mypackage/
├── mypackage.el
├── LICENSE
└── README.md
Create recipes/mypackage in MELPA repository:
(mypackage :fetcher github
:repo "user/mypackage"
:files (:defaults "icons/*.png"))
Fetchers:
:fetcher github - GitHub repository:fetcher gitlab - GitLab repository:fetcher codeberg - Codeberg repositoryFiles:
:defaults - Standard .el files"subdir/*.el"Before submission:
-- separatorpackage-lint passescheckdoc cleanPackage-Requiresprovide matches filenameBuild recipe locally:
make recipes/mypackage
Install from file:
M-x package-install-file RET /path/to/mypackage.el
Test in clean Emacs:
emacs -Q -l package -f package-initialize -f package-install-file mypackage.el
(defvar my-mode-map
(let ((map (make-sparse-keymap)))
(define-key map (kbd "C-c C-c") #'my-mode-command)
map)
"Keymap for `my-mode'.")
(define-derived-mode my-mode fundamental-mode "My"
"Major mode for editing My files.
\\{my-mode-map}"
(setq-local comment-start "#")
(setq-local comment-end ""))
Provide hook for customization:
(defvar my-mode-hook nil
"Hook run when entering `my-mode'.")
(define-derived-mode my-mode fundamental-mode "My"
...
(run-hooks 'my-mode-hook))
Use autoload for file associations:
;;;###autoload
(add-to-list 'auto-mode-alist '("\\.my\\'" . my-mode))
;;;###autoload
(define-minor-mode my-minor-mode
"Toggle My Minor Mode.
When enabled, provides X functionality."
:global t
:lighter " My"
:group 'my-package
(if my-minor-mode
(my-minor-mode--enable)
(my-minor-mode--disable)))
;;;###autoload
(define-minor-mode my-local-mode
"Toggle My Local Mode in current buffer."
:lighter " MyL"
:keymap my-local-mode-map
(if my-local-mode
(add-hook 'post-command-hook #'my-local-mode--update nil t)
(remove-hook 'post-command-hook #'my-local-mode--update t)))
Collection of functions for other packages to use:
;;; mylib.el --- Utility functions -*- lexical-binding: t; -*-
;; Author: Name
;; Version: 1.0.0
;;; Commentary:
;; Library of utility functions. Not a standalone package.
;;; Code:
(defun mylib-helper (x)
"Help with X."
...)
(provide 'mylib)
;;; mylib.el ends here
End-user feature with commands:
;;; mypackage.el --- User feature -*- lexical-binding: t; -*-
;; Package-Requires: ((emacs "25.1"))
;; Keywords: convenience
;;; Code:
;;;###autoload
(defun mypackage-start ()
"Start mypackage."
(interactive)
...)
(provide 'mypackage)
;;; mypackage.el ends here
(defcustom my-package-backend 'default
"Backend to use for processing.
Possible values:
`default' - Use built-in backend
`external' - Use external program
`auto' - Detect automatically"
:type '(choice (const :tag "Default" default)
(const :tag "External" external)
(const :tag "Auto-detect" auto))
:group 'my-package)
(defvar my-package-before-save-hook nil
"Hook run before saving with my-package.
Functions receive no arguments.")
(defun my-package-save ()
"Save current state."
(run-hooks 'my-package-before-save-hook)
...)
(defcustom my-package-format-function #'my-package-default-format
"Function to format output.
Called with one argument (the data to format).
Should return formatted string."
:type 'function
:group 'my-package)
(when (featurep 'some-package)
;; Integration with some-package
...)
(defun my-package-process (input)
"Process INPUT."
(unless input
(user-error "No input provided"))
(unless (stringp input)
(user-error "Input must be string, got %s" (type-of input)))
...)
(defun my-package--internal ()
"Internal function."
(unless (my-package--valid-state-p)
(error "Invalid state: %s" my-package--state))
...)
(defun my-package-try-operation ()
"Attempt operation, return nil on failure."
(condition-case err
(my-package--do-operation)
(file-error
(message "File error: %s" (error-message-string err))
nil)
(error
(message "Operation failed: %s" (error-message-string err))
nil)))
Use autoload to defer loading:
;;;###autoload
(defun my-package-start ()
"Start my-package."
(interactive)
(require 'my-package-core)
(my-package-core-start))
Byte-compile packages for performance:
emacs -batch -f batch-byte-compile mypackage.el
Check warnings:
M-x byte-compile-file RET mypackage.el
(defvar my-package--cache nil
"Cached data.")
(defun my-package-get-data ()
"Get data, using cache if available."
(or my-package--cache
(setq my-package--cache (my-package--compute-data))))
;;; mypackage-tests.el --- Tests -*- lexical-binding: t; -*-
(require 'ert)
(require 'mypackage)
(ert-deftest mypackage-test-basic ()
(should (equal (mypackage-process "input") "expected")))
(ert-deftest mypackage-test-error ()
(should-error (mypackage-process nil) :type 'user-error))
Run tests:
M-x ert RET t RET
(require 'buttercup)
(require 'mypackage)
(describe "mypackage-process"
(it "handles valid input"
(expect (mypackage-process "input") :to-equal "expected"))
(it "signals error for nil"
(expect (mypackage-process nil) :to-throw 'user-error)))
Requirements:
Submit to emacs-devel@gnu.org
Requirements:
Submit PR to https://github.com/melpa/melpa
Requires Git tags for versions:
git tag -a v1.0.0 -m "Release 1.0.0"
git push origin v1.0.0
Recipe includes :branch:
(mypackage :fetcher github
:repo "user/mypackage"
:branch "stable")
Include above Commentary section:
;;; mypackage.el --- Description -*- lexical-binding: t; -*-
;; Copyright (C) 2025 Author Name
;; SPDX-License-Identifier: GPL-3.0-or-later
;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;;; Commentary:
Include full license text in repository root.
Common choices:
mypackage-pkg.el:
(define-package "mypackage" "1.0.0"
"Brief description of package"
'((emacs "26.1")
(dash "2.19.1"))
:keywords '("convenience" "tools")
:authors '(("Author Name" . "email@example.com"))
:maintainer '("Maintainer" . "email@example.com")
:url "https://github.com/user/mypackage")
mypackage/
├── mypackage.el ; Main entry point with autoloads
├── mypackage-core.el ; Core functionality
├── mypackage-ui.el ; UI components
├── mypackage-utils.el ; Utilities
├── mypackage-pkg.el ; Package descriptor
└── mypackage-tests.el ; Tests (not packaged)
Each file provides named feature:
;; mypackage.el
(provide 'mypackage)
;; mypackage-core.el
(provide 'mypackage-core)
;; mypackage-ui.el
(provide 'mypackage-ui)
Main file requires subfeatures:
;;; mypackage.el --- Main file
(require 'mypackage-core)
(require 'mypackage-ui)
(provide 'mypackage)
(defun mypackage-new-name ()
"New function name."
...)
(define-obsolete-function-alias
'mypackage-old-name
'mypackage-new-name
"1.5.0"
"Use `mypackage-new-name' instead.")
(defvar mypackage-new-option t
"New option.")
(define-obsolete-variable-alias
'mypackage-old-option
'mypackage-new-option
"1.5.0"
"Use `mypackage-new-option' instead.")
(when (version< emacs-version "26.1")
(error "Mypackage requires Emacs 26.1 or later"))
;; Feature-based check preferred
(unless (fboundp 'some-function)
(error "Mypackage requires some-function"))
Emacs package conventions ensure quality, compatibility, and discoverability:
Following these conventions enables smooth integration with package.el, acceptance into package archives, and positive user experience.