| name | package-conventions |
| description | Emacs Lisp package development standards and conventions |
Emacs Package Conventions
Comprehensive guide to Emacs Lisp package development standards, covering naming, structure, metadata, and distribution requirements.
Overview
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.
Package Types
Simple Package
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
Multi-File Package
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")
File Header Conventions
Required Headers
Simple package (single .el):
;;; filename.el --- description;; Author:;; Version:or;; Package-Version:;;; Commentary:;;; Code:(provide 'feature-name);;; filename.el ends here
Standard Headers
;;; 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:
Lexical Binding
Always enable lexical binding:
;;; mypackage.el --- Description -*- lexical-binding: t; -*-
Required for modern Emacs development and MELPA acceptance.
Naming Conventions
Package Prefix
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
Symbol Naming Rules
Public vs Private:
- Public:
prefix-name - Private:
prefix--name(double dash)
Variable types:
- Function variable:
prefix-hook-function - Hook:
prefix-mode-hook - Option:
prefix-enable-feature - Local variable:
prefix--internal-state
Special cases:
- Commands can omit prefix if memorable:
list-frobsinfrobpackage - Major modes:
prefix-mode - Minor modes:
prefix-minor-mode
Case Convention
Use lowercase with hyphens (lisp-case):
(defun my-package-do-something () ; ✓
...)
(defun myPackageDoSomething () ; ✗ Wrong
...)
Package Metadata
Version Format
Semantic versioning: MAJOR.MINOR.PATCH
;; Version: 1.2.3
For snapshot builds:
;; Package-Version: 1.2.3-snapshot
;; Version: 1.2.3
Dependencies
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")
Keywords
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 structures
Check available: M-x describe-variable RET finder-known-keywords
Code Organization
Feature Provision
Always end with provide:
(provide 'mypackage)
;;; mypackage.el ends here
Feature name must match file name (without .el).
Loading Behavior
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))
Autoload Cookies
Mark interactive commands for autoloading:
;;;###autoload
(defun my-package-start ()
"Start my-package."
(interactive)
...)
;;;###autoload
(define-minor-mode my-mode
"Toggle My Mode."
...)
Group and Custom Variables
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)
Documentation Standards
Docstrings
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)
Checkdoc Compliance
Verify documentation:
M-x checkdoc RET
Requirements:
- First line ends with period
- First line fits in 80 columns
- Argument names in CAPS
- References to symbols quoted with `symbol'
- No spelling errors
Code Quality
Required Tools
package-lint:
M-x package-lint-current-buffer
Checks:
- Header format
- Dependency declarations
- Symbol naming
- Autoload cookies
flycheck-package:
(require 'flycheck-package)
(flycheck-package-setup)
Real-time package.el validation.
Common Issues
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))))
MELPA Submission
Repository Requirements
Source control:
- Git or Mercurial only
- Official repository (no forks)
- Contains LICENSE file
Structure:
mypackage/
├── mypackage.el
├── LICENSE
└── README.md
Recipe Format
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 repository
Files:
:defaults- Standard.elfiles- Custom patterns:
"subdir/*.el"
Quality Checklist
Before submission:
- Lexical binding enabled
- All public symbols prefixed
- Private symbols use
--separator - Docstrings complete and accurate
-
package-lintpasses -
checkdocclean - Dependencies declared in
Package-Requires - Autoloads on interactive commands
-
providematches filename - LICENSE file present
- No loading side effects
- Keywords from standard list
Testing Locally
Build 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
Major Mode Conventions
Mode Definition
(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 ""))
Mode Hooks
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))
Auto-mode Association
Use autoload for file associations:
;;;###autoload
(add-to-list 'auto-mode-alist '("\\.my\\'" . my-mode))
Minor Mode Conventions
Global Minor 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)))
Buffer-local Minor Mode
;;;###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)))
Library vs Package
Library
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
Package
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
Common Patterns
Configuration Option
(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)
Hook Variable
(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)
...)
Function Variable
(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)
Feature Check
(when (featurep 'some-package)
;; Integration with some-package
...)
Error Handling
User Errors
(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)))
...)
Regular Errors
(defun my-package--internal ()
"Internal function."
(unless (my-package--valid-state-p)
(error "Invalid state: %s" my-package--state))
...)
Condition Handling
(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)))
Performance Considerations
Deferred Loading
Use autoload to defer loading:
;;;###autoload
(defun my-package-start ()
"Start my-package."
(interactive)
(require 'my-package-core)
(my-package-core-start))
Compilation
Byte-compile packages for performance:
emacs -batch -f batch-byte-compile mypackage.el
Check warnings:
M-x byte-compile-file RET mypackage.el
Lazy Evaluation
(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))))
Testing Packages
ERT Tests
;;; 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
Buttercup Tests
(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)))
Distribution
GNU ELPA
Requirements:
- GPL-compatible license
- Copyright assignment to FSF (for core packages)
- High quality standards
Submit to emacs-devel@gnu.org
MELPA
Requirements:
- Git/Mercurial repository
- Standard package format
- Quality checks pass
Submit PR to https://github.com/melpa/melpa
MELPA Stable
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")
License Conventions
GPL Boilerplate
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:
LICENSE File
Include full license text in repository root.
Common choices:
- GPL-3.0-or-later
- GPL-2.0-or-later
- MIT (for non-GNU repositories)
Multi-File Packages
Package Descriptor
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")
File Organization
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)
Feature Naming
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)
Migration and Compatibility
Obsolete Functions
(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.")
Obsolete Variables
(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.")
Version Checking
(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"))
Summary
Emacs package conventions ensure quality, compatibility, and discoverability:
- Use standard file header format with required metadata
- Enable lexical binding in all files
- Prefix all public symbols with package name
- Use double-dash for private symbols
- Write comprehensive docstrings
- Mark interactive commands with autoload cookies
- Provide customization group and options
- Don't modify Emacs behavior on load
- Include LICENSE file
- Test with package-lint and checkdoc
- Follow MELPA guidelines for distribution
- Use semantic versioning
- Declare all dependencies
- End files with provide statement
Following these conventions enables smooth integration with package.el, acceptance into package archives, and positive user experience.