- Elisp Cheatsheet for Python Programmers
- Table of Contents
- Guiding Principles
- Collections
- Comparison Functions
- Sequence Types
- Non-Mutating Python Sequence to Elisp List Translations
- Non-Mutating Python Sequence to Elisp Vector Translations
- Non-Mutating Python Sequence to Elisp Sequence Translations
- Mutating Python Sequence to Elisp List Translations
- Mutating Python Sequence to Elisp Vector Translations
- Mutating Python Sequence to Elisp Sequence Translations
- Map Types
- Python String to Elisp String
- File I/O
- License
Elisp Cheatsheet for Python Programmers
This document is for readers who are familiar with the Python programming language and wish to apply that knowledge to writing Emacs Lisp (Elisp). It is intended to be a “cheat sheet”/quick reference and should not be considered a primary source for either Python or Emacs APIs.
❗ This is a work in progress. Constructive feedback is encouraged.
Version: 0.2.1
Table of Contents TOC_3
- Elisp Cheatsheet for Python Programmers
- Guiding Principles
-
- Comparison Functions
-
- Non-Mutating Python Sequence to Elisp List Translations
- Non-Mutating Python Sequence to Elisp Vector Translations
- Non-Mutating Python Sequence to Elisp Sequence Translations
- Mutating Python Sequence to Elisp List Translations
- Mutating Python Sequence to Elisp Vector Translations
- Mutating Python Sequence to Elisp Sequence Translations
- Python String to Elisp String
- File I/O
- License
Guiding Principles
- This document uses Python 3.12 and Emacs Lisp 29.1+ APIs from built-in packages.
-
Python code translated to Elisp emphasizes using generic (aka polymorphic) functions.
- This lowers the cognitive load of working with different Elisp sequence types (list, vector) and map types (hash-table, alist).
- The Emacs packages
seq.el
andmap.el
do the heavy lifting here for sequence and map types respectively.
- This document aims to provide guidance for a proficient Python programmer to implement Elisp code using familiar programming abstractions without surprise.
- Performance is at best a tertiary concern.
Collections
Comparison Functions
In Elisp, the comparison function used to disambiguate elements in a sequence-type collection or keys in a map-type collection is significant. When in doubt, it is better to specify the comparison function than trust (hope) that the default comparison function will behave to developer intent. Info (elisp) Equality Predicates details the built-in comparison functions.
That said, here is some general guidance. Use the comparison function:
=
to compare numberseq
to compare object identity.eql
to compare numbers but also take into account type. For example comparing an integer to its numerically equivalent float will returnnil
.equal
to compare if the objects have equal components.
Python | Elisp | Notes |
---|---|---|
is |
eq |
|
== |
equal |
Depending on the type of object to compare, you may have to resort to writing a custom comparison function.
Sequence Types
There are three basic sequence types in Python: list, tuple, and range. This document will cover only Elisp translation of Python list and tuple types and subsequent reference to Python sequences should be understood to not include range. For Elisp sequence types, this document will cover list and vector types.
The Elisp list type is a tree data structure with a linked-list style abstraction for accessing its nodes. Elisp also offers a general purpose array type called a vector.
The following legend describes types for variables used in the translations.
s: sequence x: any t: sequence n: integer i: integer j: integer cmp: comparison function
Non-Mutating Python Sequence to Elisp List Translations
Elisp list specific translations.
Python Sequence | Elisp List | Notes |
---|---|---|
s = [] , s = list() , s = (n1, n2, …) |
(setq s ()) , (setq s nil) |
nil has extra meaning in Elisp as it represents an empty list. |
list(range(0, n)) |
(number-sequence 0 (1- n)) |
|
s * n or n * s |
(cl-loop repeat n append s) , (apply #'append (make-list n s)) |
cl-loop needs (require 'cl-lib) . |
x in s |
(member x s) |
member can be used if cmp is equal . |
x not in s |
(not (member x s)) |
member can be used if cmp is equal . |
s + t |
(seq-concatenate 'list s t) , (append s t) |
|
s[0] |
(car s) |
|
s[-1] |
(car (last s)) |
Non-Mutating Python Sequence to Elisp Vector Translations
Elisp vector specific translations.
Python Sequence | Elisp Vector | Notes |
---|---|---|
s = [] |
(setq s (vector)) |
|
s + t |
(seq-concatenate 'vector s t) , (vconcat s t) |
|
s[i] |
(aref s i) |
Non-Mutating Python Sequence to Elisp Sequence Translations
These translations work on either Elisp list or vector types.
Python Sequence | Elisp Sequence (List or Vector) | Notes |
---|---|---|
x in s |
(seq-contains-p s x #'cmp) |
Make sure cmp will compare the element type! |
x not in s |
(not (seq-contains-p s x #'cmp)) |
Make sure cmp will compare the element type! |
map(lambda a: a * n, s) |
(seq-map (lambda (a) (* n a)) s) |
|
s[i] |
(seq-elt s i) |
|
s[i:j] |
(seq-subseq s i j) |
|
s[i:] |
(seq-subseq s i) |
|
s[i:j:k] |
||
len(s) |
(seq-length s) , (length s) |
|
min(s) |
(seq-min s) |
Elements of s must be orderable. |
max(s) |
(seq-max s) |
Elements of s must be orderable. |
s.index(x) |
(seq-position s x) |
|
s.count(x) |
(seq-count (lambda (a) (cmp x a)) s) |
|
s[0] |
(seq-first s) |
|
s[-n] |
(seq-first (seq-subseq s -n)) |
|
if not s: |
(seq-empty-p s) |
Mutating Python Sequence to Elisp List Translations
Elisp analogs to the Python list methods to handle insertion, appending, and updating are left to the developer to implement. Arguably, the omission of these functions is reluctance on the part of Emacs Core to make the trade-off design decisions required to implement them.
The following Elisp translations will mutate the original input s
.
Python Sequence | Elisp List | Notes |
---|---|---|
s.append(x) |
(setq s (nreverse (cons x (reverse s)))) |
This implementation is slow if s is large. |
s.clear() |
(setq s ()) , (setq s nil) |
|
s.extend(t) |
(setq s (append s t)) |
|
s *=n |
(setq s (cl-loop repeat n append s)) |
cl-loop needs (require 'cl-lib) . |
s.push(x) |
(push x s) |
|
s.pop() |
(pop s) |
|
s.insert(0, x) |
(push s x) |
|
s.insert(i, x) |
(setq s (append (seq-subseq s 0 i) (cons x (seq-subseq s i)))) |
Mutating Python Sequence to Elisp Vector Translations
Python Sequence | Elisp Vector | Notes |
---|---|---|
s[i] = x |
(aset s i x) |
|
s.clear() |
(setq s (vector)) |
|
s.remove(x) |
(remove x s) |
Mutating Python Sequence to Elisp Sequence Translations
These translations work on either Elisp list or vector types.
Python Sequence | Elisp Sequence (List or Vector) | Notes |
---|---|---|
s[i] = x |
(setf (seq-elt s i) x) |
|
s[i:j] = t |
||
del s[i:j] |
(setq s (append (seq-subseq s 0 i) (seq-subseq s j))) |
|
del s[i] |
(setq s (seq-remove-at-position s i)) | |
s[i:j:k] = t |
||
del s[i:j:k] |
||
s.copy() |
(seq-copy s) |
|
s.extend(t) |
(setq s (append s t)) |
|
s *=n |
(setq s (cl-loop for _ from 1 to n nconc (seq-copy s))) |
|
s.remove(x) |
(setq s (seq-remove (lambda (a) (cmp x a)) s)) , (setq s (remove x s)) |
Note Elisp translation presumes only one instance of x is in s , as seq-remove will remove all instances of x whereas in Python s.remove(x) will only remove the first instance. |
s.reverse() |
(setq s (seq-reverse s)) , (setq s (reverse s)) , (setq s (nreverse s)) |
nreverse may destructively mutate s . |
Map Types
Python Dictionary to Elisp Hash Table
The Elisp hash-table
is the most straightforward analog to a Python dictionary. That said, there are gotchas, particularly around hash-table
creation. If the keys are of type string, then the key comparison should be set to the function equal
via the :test
slot. If :test
is omitted the default function eql
is used which compares numbers.
d: dictionary/hash-table k: key v: value cmp: comparison function
Python | Elisp | Notes |
---|---|---|
d = dict() , d = {} |
(setq d (make-hash-table :test #'cmp)) |
If :test is omitted, default cmp is eql . |
list(d) |
(map-keys d) |
|
len(d) |
(map-length d) |
|
d[k] |
(map-elt d k) |
|
d[k] = v |
(map-put! d k v) |
|
del d[k] |
(map-delete d k) |
|
k in d |
(map-contains-key d k) |
|
k not in d |
(not (map-contains-key d k)) |
|
iter(d) |
||
d.clear() |
(clrhash d) |
|
d.copy() |
(map-copy d) |
|
d.get(k) |
(map-elt d k) |
|
d.items() |
(map-pairs d) |
|
d.keys() |
(map-keys d) |
|
d.pop(k) |
||
d.popitem() |
||
reversed(d) |
||
d.values() |
(map-values d) |
|
(map-insert d k v) |
Like map-put! but does not mutate d . |
Python Dictionary to Elisp Association List (alist)
An alist is a convention to construct a basic list such that key-value semantics can be applied to it. An alist is allowed to possess degenerate keys (that is, keys are not necessarily unique!). This is because in truth, an alist is still a list with no actual enforcement of how (key, value) pairs are stored in it. IMHO Elisp alists are an abomination, albeit a pragmatic one. Conventional Elisp wisdom arguing for alist usage boils down to convenient serialization and the notion that in practice, alist sizes are small enough to not merit the overhead of using hash-tables.
Regardless, my guidance is to exercise caution when translating Python dictionary code to an Elisp alist.
d: dictionary/alist k: key v: value
Python | Elisp | Notes |
---|---|---|
d = dict() , d = {} |
(setq d (list)) |
|
list(d) |
(map-keys d) |
|
len(d) |
(map-length d) |
|
d[k] |
(map-elt d k) |
Type-specific behavior requires specifying test function t . |
d[k] = v |
(map-put! d k v) |
This only works if d is not nil. To initialize use (push '(k . v) d) . |
del d[k] |
(setq d (map-delete d k)) |
Type-specific behavior is dependent on key type. 😞 |
k in d |
(map-contains-key d k) |
|
k not in d |
(not (map-contains-key d k)) |
|
iter(d) |
||
d.clear() |
(setq d (list)) |
|
d.copy() |
(map-copy d) |
|
d.get(k) |
(map-elt d k) |
|
d.items() |
(map-pairs d) |
|
d.keys() |
(map-keys d) |
|
d.pop(k) |
||
d.popitem() |
||
reversed(d) |
||
d.values() |
(map-values d) |
|
(map-insert d k v) |
Like map-put! but does not mutate d . Also does not check uniqueness. |
Python Dictionary to Elisp Property List (plist)
TBD
Looping
Two functions which can iterate through a map are map-do
and map-apply
. Shown below are the Python translated equivalents.
(map-do f d) ; return nil
def map_do(d):
for k,v in d.items():
f(k, v)
(map-apply f d) ; return results of f applied to each element of d as a list
def map_apply(d):
results = []
for k,v in d.items():
results.append(f(k, v))
return results
Python String to Elisp String
s: string a: string b: string c: string sep: separator string strs: list of strings
Python | Elisp | Notes |
---|---|---|
"" |
(make-string 0 ? ) , "" |
|
a + b + c |
(concat a b c) |
|
s.strip() |
(string-clean-whitespace s) |
|
s.capitalize() |
(capitalize s) |
|
s.casefold() |
||
s.center(width) |
||
s.count(sub) |
||
s.encode(encoding) |
||
s.endswith(suffix) |
(string-suffix-p suffix s) |
|
s.expandtabs(tabsize) |
||
s.find(sub) |
(string-search sub s) |
|
s.format(*args, **kwargs) |
(format fmt args…) |
|
s.index(sub) |
(string-search sub s) |
|
s.isalnum() |
(string-match "^[[:alnum:]]*$" s) |
|
s.isalpha() |
(string-match "^[[:alpha:]]*$" s) |
|
s.isascii() |
(string-match "^[[:ascii:]]*$" s) |
|
s.isdecimal() |
||
s.isdigit() |
(string-match "^[[:digit:]]*$" s) |
|
s.islower() |
(string-match "^[[:lower:]]*$" s) |
case-fold-search must be nil. |
s.isnumeric() |
||
s.isprintable() |
(string-match "^[[:print:]]*$" s) |
|
s.isspace() |
(string-match "^[[:space:]]*$" s) |
|
s.istitle() |
||
s.isupper() |
(string-match "^[[:upper:]]*$" s) |
case-fold-search must be nil. |
sep.join(strs) |
(string-join strs sep) |
|
s.ljust(width) |
||
s.lower() |
(downcase s) |
|
s.lstrip() |
(string-trim-left s) |
|
s.removeprefix(prefix) |
(string-remove-prefix prefix s) |
|
s.removesuffix(suffix) |
(string-remove-suffix suffix s) |
|
s.replace(old, new, count=-1) |
(string-replace old new s) |
|
s.rfind(sub) |
||
s.rindex(sub) |
||
s.rjust(width) |
||
s.rsplit(sep) |
||
s.rstrip() |
(string-trim-right s) |
|
s.split(sep) |
(split-string s sep) |
|
s.splitlines() |
(string-lines s) |
|
s.startswith(prefix) |
(string-prefix-p prefix s) |
|
s.strip() |
(string-trim s) |
|
s.swapcase() |
||
s.title() |
(upcase-initials s) |
|
s.upper() |
(upcase s) |
|
s.zfill(width) |
||
s1 == s2 |
(string-equal s1 s2) , (string= s1 s2) |
File I/O
The in-memory representation of a file in Emacs is a buffer, whose closest analog in a general purpose language like Python is a file handle. A common pattern is to read the contents of a file into list of strings, each string separated by a newline ("\n").
Here is an example of this in Python.
def read_file_lines(filename):
with open(filename, "r") as infile:
lines = infile.readlines()
return lines
for line in read_file_lines(filename):
print(line.rstrip('\n'))
Here is an Elisp equivalent.
(defun read-file-lines (filename)
"Load FILENAME into a buffer and read each line."
(with-temp-buffer
;; Insert the contents of the file into the temporary buffer
(insert-file-contents filename)
;; Move to the beginning of the buffer
(goto-char (point-min))
;; Initialize an empty list to hold the lines
(let ((lines '()))
;; Loop until the end of the buffer is reached
(while (not (eobp))
;; Read the current line
(let ((line (string-trim-right (thing-at-point 'line t))))
;; Add the line to the list
(push line lines))
;; Move to the next line
(forward-line 1))
;; Return the lines in the correct order
(nreverse lines))))
;; Example usage:
(let ((lines (read-file-lines "somefile.log")))
(dolist (line lines)
(message "%s" line)))
Writing an Elisp list to a file is illustrated in the following example.
(defun write-strings-to-file (strings filename)
"Write a list of STRINGS to FILENAME, one string per line."
(with-temp-file filename
;; Iterate over each string in the list
(dolist (str strings)
;; Insert the string followed by a newline character
(insert str "\n"))))
;; Example usage:
(let ((my-strings (read-file-lines "somefile.log"))
(file-path "some-other-file.log"))
(write-strings-to-file my-strings file-path))
Although the above examples work as advertised, conventional Elisp wisdom frowns upon pipeline style processing of collections arguing that:
- Elisp has been optimized to work in-place with buffer contents and that transformations should be made directly to the buffer content.
- Pipeline style processing of collections is slow. If you are going to process a large log file, using Elisp is not the right tool for the job.
It is left to the reader whether to heed this guidance.
License
© 2025. This work is openly licensed via CC BY 4.0.