Skip to content

Instantly share code, notes, and snippets.

@lispstudent
Created May 12, 2026 15:48
Show Gist options
  • Select an option

  • Save lispstudent/4cf841027b287c3e36bd85592ed6910e to your computer and use it in GitHub Desktop.

Select an option

Save lispstudent/4cf841027b287c3e36bd85592ed6910e to your computer and use it in GitHub Desktop.
python and Common Lisp

Common Lisp Cheatsheet for Python Programmers

This document is for readers familiar with Python who wish to apply that knowledge to writing Common Lisp (CL). It is a quick reference, not a primary source for either Python or CL APIs.

It is patterned after the structure of Charles Y. Choi's Elisp Cheatsheet for Python Programmers and intended as its CL counterpart.

  • Version: 0.1.0
  • Last updated: 2026-05-12

Guiding Principles

  • Python is 3.13.
  • Common Lisp is ANSI CL (X3.226-1994) as implemented by current SBCL, CCL, LispWorks 8.x, ECL, and friends. Where features come from third-party libraries, the library is named in the table cell or section heading; the versions targeted are:
  • Translations favour idiomatic, generic code over micro-optimisation. Common Lisp's sequence functions (length, elt, subseq, map, reduce, find, position, count, remove, substitute, sort) are polymorphic over list, vector, and string. Prefer them when a single piece of code should work on more than one sequence type.
  • Performance is a tertiary concern. Where a translation has a well-known performance pitfall (e.g. append to grow a list), it is noted.
  • This is a draft; corrections welcome.

Symbol & Package Conventions

Throughout, unqualified names like iota, hash-table-keys, dict, take, scan are not in the CL package. They live in third-party packages. A typical preamble:

;;;; common-lisp-for-python-examples.lisp  --- version 0.1.0  2026-05-12
(defpackage #:cl-for-python-examples
  (:use #:cl)
  (:local-nicknames (#:a   #:alexandria)
                    (#:s   #:serapeum)
                    (#:ppcre #:cl-ppcre)
                    (#:re  #:cl-ppcre))) ; if you prefer "re." for regex calls
(in-package #:cl-for-python-examples)

Code samples below assume those nicknames (a:, s:, ppcre:).


Collections

Comparison Functions

Common Lisp's standard equality predicates form a tower; you must pick one for hash-table construction (:test), find/position/member (:test), equalp vs equal, etc.

Predicate Compares
eq Object identity. Reliable only for symbols and eq-uniqued objects.
eql Like eq but also numbers of the same type and same value, and same chars. Default for hash tables, case, member, etc.
equal Structural equality for lists, strings, bit-vectors, pathnames.
equalp Like equal but case-insensitive for strings/chars, type-loose for numbers, recurses into arrays/structures/hash-tables.
= Numeric equality across numeric types ((= 1 1.0)T).
string= Case-sensitive string equality.
string-equal Case-insensitive string equality.

Reference: CLHS — Equality Predicates.

Python Common Lisp Notes
is (eq a b)
== on numbers (= a b) Works across numeric types.
== on strings (string= a b) Case-sensitive.
== on general structures (equal a b) / (equalp a b) equalp for case-insensitive / recursive.

A custom comparator is just a function; pass it via :test, :key, or :test-not.

Sequence Types

Common Lisp has a single abstract sequence type with two main concrete subtypes that matter here:

  • list — singly-linked cons cells. '(1 2 3). (list 1 2 3).
  • vector — one-dimensional arrays. #(1 2 3). (vector 1 2 3).
    • string is a specialised vector of characters. "hi".
    • bit-vector is a specialised vector of bits.

Use the legend below for variables in the tables:

s, t : sequence (list or vector)
x    : any object
n, i, j, k : integers
cmp  : comparison/test function

Creation

Python Common Lisp Notes
[] '(), (list), nil nil is the empty list in CL.
[1, 2, 3] (list 1 2 3), '(1 2 3) Quoted literals are read-only.
list(range(n)) (loop for i below n collect i), (a:iota n) a:iota from Alexandria.
list(range(a, b)) (loop for i from a below b collect i), (a:iota (- b a) :start a)
list(range(a, b, k)) (loop for i from a below b by k collect i), (a:iota (ceiling (- b a) k) :start a :step k)
[] (vector) #(), (vector), (make-array 0)
[0] * n (make-list n :initial-element 0), (make-array n :initial-element 0)
s * n (loop repeat n nconc (copy-list s)) (list); (s:repeat-sequence s n) (any seq) s:repeat-sequence from Serapeum.
s + t (append s t) (lists), (concatenate 'list s t), (concatenate 'vector s t), (concatenate 'string s t) concatenate is generic, result type required.

Indexing & Slicing — Non-mutating

Python Common Lisp Notes
s[i] (elt s i) Generic; O(n) on lists. Use (nth i s) for lists, (aref v i) for vectors when speed matters.
s[-1] (elt s (1- (length s))), (s:lastcar s) (list), (car (last s)) (list) s:lastcar from Serapeum.
s[-n] (elt s (- (length s) n)), (car (last s n)) (list)
s[i:j] (subseq s i j) Returns a fresh sequence.
s[i:] (subseq s i)
s[:j] (subseq s 0 j)
s[i:j:k] (loop for idx from i below j by k collect (elt s idx)) No built-in stride.
len(s) (length s)
x in s (find x s :test #'cmp), (member x s :test #'cmp) (list only) Both return generalised-boolean; member returns the tail.
x not in s (not (find x s :test #'cmp))
s.index(x) (position x s :test #'cmp)
s.count(x) (count x s :test #'cmp)
min(s) (reduce #'min s) Or (loop for x in s minimize x).
max(s) (reduce #'max s)
sum(s) (reduce #'+ s), (loop for x in s sum x)
s[0] (first s) / (car s) (list), (elt s 0) (generic)
not s (null s) (list), (zerop (length s)) (generic), (a:emptyp s) (Alexandria)
s == t (equal s t) (lists/strings), (equalp s t) (recursive incl. vectors)
reversed(s) (reverse s) Non-destructive. Returns same type as s.
sorted(s) (sort (copy-seq s) #'<) sort may destructively modify s — always copy-seq first if you need to keep the original. stable-sort similarly.
sorted(s, key=f, reverse=True) (sort (copy-seq s) #'> :key #'f)

Mutation

sort, nreverse, delete, nconc, nsubstitute, etc. are destructive; their non-n counterparts are functional.

For lists, growing efficiently means push + nreverse at the end, not repeated append.

Python Common Lisp Notes
s.append(x) (list) (setf s (nconc s (list x))), idiomatic: build with push then nreverse Single append-grow is O(n) per call.
s.append(x) (vector w/ fill-pointer) (vector-push-extend x s) s must be :adjustable t :fill-pointer t. See CLHS vector-push-extend.
s.pop() (list) (pop s) pop/push are macros over generalised places.
s.pop(0) ≡ pop front (pop s)
s.insert(0, x) (push x s)
s.insert(i, x) (setf s (nconc (subseq s 0 i) (cons x (subseq s i))))
s.remove(x) (first occurrence) (setf s (remove x s :count 1 :test #'cmp)) Without :count, removes all.
del s[i] (setf s (nconc (subseq s 0 i) (subseq s (1+ i))))
s.clear() (setf s nil) (list); for vectors with fill-pointer: (setf (fill-pointer s) 0)
s.reverse() (in place) (setf s (nreverse s))
s.extend(t) (setf s (nconc s (copy-list t)))
s.sort() (in place) (setf s (sort s #'<)) Always reassign — sort is permitted to destroy.
s[i] = x (setf (elt s i) x) Or (setf (nth i s) x) for lists, (setf (aref v i) x) for vectors.

Generic Sequence Operations (Polymorphic, Idiomatic)

These work on lists and vectors and strings.

Operation Common Lisp
Map (eager, into list) (mapcar #'f s) (lists), (map 'list #'f s) (any seq)
Map (into vector / string / same type) (map 'vector #'f s), (map 'string #'f s), (map (type-of s) #'f s)
Map for side-effect, ignore result (map nil #'f s), (mapc #'f s) (lists)
Filter (remove-if-not #'pred s), (s:filter #'pred s)
Filter-out (remove-if #'pred s)
Reduce / fold-left (reduce #'f s), (reduce #'f s :initial-value v0)
Reduce right (reduce #'f s :from-end t)
Any (some #'pred s)
All (every #'pred s)
None (notany #'pred s)
Zip (mapcar #'list a b ...) (lists), (map 'list #'list a b ...) (any)
Enumerate (loop for x in s for i from 0 collect (list i x))
Flatten one level (reduce #'append list-of-lists), (a:mappend #'identity list-of-lists)
Flatten deep (a:flatten tree) (Alexandria, lists only)

Augmentations from Alexandria & Serapeum

Python idiom Common Lisp (lib) Library — Function
list(range(n)) (a:iota n) Alexandria — iota
itertools.cycle-like initialisation (make-list n :initial-element x); for cycling iteration use LOOP or s:repeat-sequence Serapeum — repeat-sequence
random.shuffle(s) (returns new) (a:shuffle (copy-seq s)) Alexandria — shuffle
random.choice(s) (a:random-elt s) Alexandria — random-elt
s[:n] (cheap) (s:take n s) Serapeum — take
s[n:] (cheap) (s:drop n s) Serapeum — drop
s[i:j] w/ negative indices (s:slice s i j) Serapeum — slice (accepts negative bounds)
itertools.batched(s, n) (3.12+) (s:batches s n) Serapeum — batches
itertools.groupby(s, key=f) (sorted-runs) (s:runs s :key #'f :test #'equal) Serapeum — runs
Group-by (any order, like collections.defaultdict(list) accumulation) (s:assort s :key #'f :test #'equal) Serapeum — assort
collections.Counter(s) (s:frequencies s :test #'equal) → hash-table Serapeum — frequencies
(true_list, false_list) = partition (s:partition #'pred s) returns two values Serapeum — partition
[x for x in s if pred(x)][:n] (s:take n (s:filter #'pred s))
list(itertools.takewhile(pred, s)) (s:take-while #'pred s) Serapeum — take-while
list(itertools.dropwhile(pred, s)) (s:drop-while #'pred s) Serapeum — drop-while
min(s, key=f), max(s, key=f) (a:extremum s #'< :key #'f), (a:extremum s #'> :key #'f) Alexandria — extremum
heapq.nlargest(n, s) (s:bestn n s #'>) Serapeum — bestn
sorted(set(s), key=s.index) (de-dup, preserve order) (remove-duplicates s :from-end t) — works because :from-end t keeps the first occurrence.

remove-duplicates gotcha: by default it keeps the last occurrence (because it scans front-to-back and discards later duplicates from the already-seen prefix). Use :from-end t to keep the first occurrence, which is what most Python programmers expect. (CLHS — remove-duplicates)

Map Types

Common Lisp offers three map-like structures:

  • Association list (alist)((k1 . v1) (k2 . v2) …), a list of cons cells.
  • Property list (plist)(k1 v1 k2 v2 …), a flat list alternating keys/values.
  • Hash table — a true hash table; closest analog to Python dict.

Use hash-table when you have many keys, want O(1) lookup, or have string keys. Use alist or plist for small, often-symbol-keyed lookup tables where readability and serialisability matter.

Python dict → Common Lisp hash-table

Reference: CLHS — make-hash-table, CLHS — gethash.

d : hash-table
k : key
v : value
Python Common Lisp Notes
d = {} (setf d (make-hash-table :test 'equal)) For string keys you MUST pass :test 'equal. Default eql won't match equal-but-not-identical strings.
d = {k: v} (let ((d (make-hash-table :test 'equal))) (setf (gethash k d) v) d), or (s:dict k v) s:dict (Serapeum) is a literal constructor.
d[k] (gethash k d) Returns two values: value, present-p.
d[k] = v (setf (gethash k d) v)
d.get(k) (gethash k d) First value is nil if missing.
d.get(k, default) (gethash k d default) Third arg to gethash is the default.
d.setdefault(k, dflt) (a:ensure-gethash k d dflt) Alexandria — ensure-gethash
del d[k] (remhash k d) Returns T if a value was removed.
k in d (nth-value 1 (gethash k d)) Use the second return value.
len(d) (hash-table-count d)
d.clear() (clrhash d)
d.copy() (a:copy-hash-table d) Alexandria — copy-hash-table
list(d) / d.keys() (a:hash-table-keys d) Alexandria.
d.values() (a:hash-table-values d) Alexandria.
d.items() (a:hash-table-alist d) Alexandria — alist form.
dict(d.items()) (a:copy-hash-table d)
dict(alist) (a:alist-hash-table alist :test 'equal)
for k, v in d.items(): f(k, v) (maphash (lambda (k v) (f k v)) d) Standard CL. Order is unspecified.
for k, v in d.items(): f(k, v) (Serapeum) (s:do-hash-table (k v d) (f k v)) Serapeum macro.
{**d1, **d2} Loop and setf gethash, or (let ((m (a:copy-hash-table d1))) (maphash (lambda (k v) (setf (gethash k m) v)) d2) m)

Why gethash returns two values: nil is a perfectly valid value in a hash table, so a single return value can't distinguish "absent" from "present, but nil". Always use the second value (present-p) to check membership, never the first.

Python dict → alist

d : alist
Python Common Lisp Notes
d = {} (setf d nil)
d = {k: v, ...} '((k1 . v1) (k2 . v2) ...), (a:alist-plist plist)
d[k] (cdr (assoc k d :test #'equal)) :test defaults to eql.
d.get(k, dflt) (or (cdr (assoc k d :test #'equal)) dflt) (only safe if no entry has value nil); for correctness: (let ((c (assoc k d :test #'equal))) (if c (cdr c) dflt))
d[k] = v (insert/update) (setf d (acons k v d)) to push a new pair on the front (later lookups by assoc find it first); or (setf (a:assoc-value d k :test #'equal) v) to update in place if present or push if absent. Alexandria — assoc-value is a setf-able accessor.
del d[k] (setf d (remove k d :key #'car :test #'equal))
k in d (assoc k d :test #'equal) (treated as boolean)
len(d) (distinct keys) (length (remove-duplicates (mapcar #'car d) :test #'equal :from-end t)) Plain (length d) counts duplicate keys.
d.keys() (mapcar #'car d)
d.values() (mapcar #'cdr d)
Iterate (loop for (k . v) in d do (f k v))

Python dict → plist

d : plist
Python Common Lisp Notes
d = {} (setf d nil)
d = {:a 1, :b 2} '(:a 1 :b 2) Keys are normally keyword symbols.
d[k] (getf d k) Default test is eq — fine for symbols/keywords, not for strings.
d.get(k, dflt) (getf d k dflt)
d[k] = v (setf (getf d k) v)
del d[k] (remf d k)
k in d (get-properties d (list k)) returns 3 values; check the third (the tail) for non-nil.
Iterate (loop for (k v) on d by #'cddr do (f k v)), or (a:doplist (k v d) (f k v)) Alexandria — doplist

Iteration

Sequence — for side-effect

for i in range(n):
    f(i)
;; Idiomatic CL:
(dotimes (i n) (f i))

;; Or, generically over any sequence:
(map nil #'f (a:iota n))

Sequence — collect results

[f(x) for x in s]
(mapcar #'f s)                 ; list -> list
(map 'list #'f s)              ; any seq -> list
(map 'vector #'f s)            ; any seq -> vector

;; With LOOP (often the clearest for non-trivial bodies):
(loop for x in s collect (f x))           ; list source
(loop for x across v collect (f x))       ; vector source

Filter

[x for x in s if pred(x)]
(remove-if-not #'pred s)
;; or
(loop for x in s when (pred x) collect x)
;; Serapeum:
(s:filter #'pred s)

Truthiness: 0, 0.0, "", and #() are all true in Common Lisp. Only nil is false. So (if x …) checks "x is not nil", not "x is nonzero". For Python-style numeric filtering, write the predicate explicitly: (remove-if (lambda (n) (zerop (mod n 2))) s).

Reduce

from functools import reduce
reduce(lambda x, y: x + y, range(10))      # 45
reduce(lambda x, y: x + y, range(10), 100) # 145
(reduce #'+ (a:iota 10))               ; => 45
(reduce #'+ (a:iota 10) :initial-value 100) ; => 145

Map iteration

for k, v in d.items():
    f(k, v)
;; hash-table:
(maphash (lambda (k v) (f k v)) d)
;; or (Serapeum):
(s:do-hash-table (k v d) (f k v))

;; alist:
(loop for (k . v) in d do (f k v))

;; plist:
(loop for (k v) on d by #'cddr do (f k v))
;; or (Alexandria):
(a:doplist (k v d) (f k v))

LOOP — Python's for for grown-ups

LOOP is the swiss-army-knife iteration macro and worth learning early. It handles range, parallel iteration, accumulation, early termination, conditional logic, and destructuring in one form.

;; for i in range(n): print(i)
(loop for i below n do (print i))

;; for a, b in zip(xs, ys): ...
(loop for a in xs for b in ys do (f a b))

;; enumerate
(loop for x in xs for i from 0 collect (list i x))

;; sum / max / min / count
(loop for x in xs sum x)
(loop for x in xs maximize x)
(loop for x in xs counting (oddp x))

;; build dict-like structures
(loop for x in xs
      for k = (key-fn x)
      collect (cons k (compute x)) into alist
      finally (return alist))

Reference: Common Lisp Cookbook — Loop.

Comprehensions

CL has no comprehension syntax. The two normal substitutes are LOOP (above) and iterate (third-party, more uniform) — but for the common cases, mapcar, remove-if-not, and loop cover Python's [x for x in s], [x for x in s if p(x)], and [f(x) for x in s if p(x)] respectively.

[f(x) for x in s if p(x)]
(mapcar #'f (remove-if-not #'p s))
;; or, single pass:
(loop for x in s when (p x) collect (f x))

Strings

CL strings are vectors of characters. All generic sequence operations (length, elt, subseq, map, reduce, find, position, remove, reverse, count, sort) work directly on them.

s, a, b : string
sub, sep : string
i, j : integer
strs : list of strings

Standard CL String Operations

Python Common Lisp Notes
"" ""
s[i] (char s i), (aref s i), (elt s i) All return a character, not a length-1 string.
s[i:j] (subseq s i j)
s[i:] (subseq s i)
s[:j] (subseq s 0 j)
len(s) (length s)
a + b (concatenate 'string a b) Use format for many pieces: (format nil "~A~A~A" a b c).
sep.join(strs) (format nil "~{~A~^~A~}" (cons sep strs)) — but cleaner with library: see s:string-join below.
s.split(sep) (ppcre:split (ppcre:quote-meta-chars sep) s) (CL-PPCRE), or (split-sequence:split-sequence #\, s) (split-sequence library) No standard split.
s.splitlines() (ppcre:split "\\r?\\n" s)
s.lower() (string-downcase s)
s.upper() (string-upcase s)
s.capitalize() (string-capitalize s) CL capitalizes every "word", more like .title(). For Python's single-word version, (concatenate 'string (string (char-upcase (char s 0))) (string-downcase (subseq s 1))).
s.title() (string-capitalize s)
s.swapcase() (map 'string (lambda (c) (cond ((upper-case-p c) (char-downcase c)) ((lower-case-p c) (char-upcase c)) (t c))) s)
s.strip() (string-trim '(#\Space #\Tab #\Newline #\Return) s)
s.lstrip() (string-left-trim '(#\Space #\Tab #\Newline #\Return) s)
s.rstrip() (string-right-trim '(#\Space #\Tab #\Newline #\Return) s)
s.startswith(p) (a:starts-with-subseq p s) (Alexandria), or (and (>= (length s) (length p)) (string= s p :end1 (length p)))
s.endswith(suf) (a:ends-with-subseq suf s) (Alexandria)
s.find(sub) (search sub s) Returns nil if not found.
s.find(sub, i) (search sub s :start2 i)
s.rfind(sub) (search sub s :from-end t)
s.index(sub) (or (search sub s) (error "substring not found"))
s.count(sub) (ppcre:count-matches (ppcre:quote-meta-chars sub) s) (CL-PPCRE); for single chars: (count #\x s)
s.replace(old, new) (ppcre:regex-replace-all (ppcre:quote-meta-chars old) s new) (CL-PPCRE), or (s:string-replace-all old s new) (Serapeum)
s.replace(old, new, k) No direct standard; loop k times with s:string-replace, or compose.
s.removeprefix(p) (if (a:starts-with-subseq p s) (subseq s (length p)) s), or (s:drop-prefix p s) (Serapeum)
s.removesuffix(p) (if (a:ends-with-subseq p s) (subseq s 0 (- (length s) (length p))) s), or (s:drop-suffix p s) (Serapeum)
s.ljust(w) / rjust(w) / center(w) (format nil "~vA" w s) / (format nil "~v@A" w s) / centre via format no-op — easier to roll your own with make-string.
s.zfill(w) (format nil "~v,'0D" w (parse-integer s)) for ints; for general padding: (format nil "~v,,,'0@A" w s)
s.format(...) (format nil "..." ...) Note: CL's format is its own language; see CLHS — Formatted Output.
f"{x:.2f}" (format nil "~,2F" x)
f"{x:d}" (format nil "~D" x)
f"{x!r}" (format nil "~S" x) ~A is "aesthetic" (no quotes), ~S is "standard" (readable).
s == t (string= s t) (case-sensitive), (string-equal s t) (case-insensitive)
s.isalpha() etc. Use every with alpha-char-p, digit-char-p, alphanumericp, upper-case-p, lower-case-p, graphic-char-p (CLHS § 13.1.4). (every #'alpha-char-p s). (every pred "") is T, matching Python's "".isalpha()False only because Python special-cases empty. Add (plusp (length s)) if you need that.

Strings are characters, not bytes. (char s 0) returns #\h, not a string "h". To get a length-1 string, (string (char s 0)) or (subseq s 0 1).

Augmentations from Serapeum

Python Common Lisp (Serapeum) Notes
sep.join(strs) (s:string-join strs sep)
s.replace(old, new) (s:string-replace-all old s new)
s.removeprefix(p) (s:drop-prefix p s)
s.removesuffix(p) (s:drop-suffix p s)
Convert string ↔ symbol (s:ensure-symbol str), (s:string-upcase-initials str)

Serapeum reference: REFERENCE.md.

Regular Expressions — CL-PPCRE

There is no regex in ANSI CL. CL-PPCRE is the de facto standard. Its syntax is Perl-compatible (close to Python's re — character classes, \d, \w, \s, non-capturing groups, named groups, backreferences).

Reference: CL-PPCRE docs.

p : pattern (string OR compiled scanner OR parse tree)
s : target string
r : replacement (string with $0..$9 backrefs, or function)
Python re CL-PPCRE Notes
re.search(p, s) → match or None (ppcre:scan p s) → 4 vals: m-start m-end reg-starts reg-ends, or NIL
re.search(p, s).group(0) (ppcre:scan-to-strings p s) → 2 vals: whole-match, #(register-strings...)
re.match(p, s) (anchored at start) (ppcre:scan (concatenate 'string "^(?:" p ")") s) or build pattern with ^
re.fullmatch(p, s) Anchor both ends: (ppcre:scan (concatenate 'string "^(?:" p ")$") s)
re.findall(p, s) (ppcre:all-matches-as-strings p s)
re.finditer(p, s) (ppcre:do-matches-as-strings (m p s) (do-something m)) (macro)
re.sub(p, r, s) (ppcre:regex-replace-all p s r) Note arg order: pattern, target, replacement.
re.sub(p, r, s, count=1) (ppcre:regex-replace p s r)
re.split(p, s) (ppcre:split p s)
re.escape(s) (ppcre:quote-meta-chars s)
re.compile(p) (ppcre:create-scanner p)
Match groups (ppcre:register-groups-bind (a b c) (p s) (list a b c))
Match groups w/ coercion (ppcre:register-groups-bind ((#'parse-integer y m d)) (p s) (encode-universal-time 0 0 0 d m y)) Coercion fn in a list.
Replace with callback over groups Pass a function as replacement to regex-replace[-all]; it receives match-start, match-end, register-arrays. See CL-PPCRE — regex-replace.
(?i) flag (?i) inside pattern, OR (ppcre:create-scanner p :case-insensitive-mode t)
Verbose / extended (?x) OR :extended-mode t to create-scanner

Example — Python re.findall(r"\d+", s) → list of ints:

;;; version 0.1.0  2026-05-12
(defun find-all-ints (s)
  "Return a list of integers parsed from every run of digits in S."
  (mapcar #'parse-integer (ppcre:all-matches-as-strings "\\d+" s)))

;; (find-all-ints "numbers: 1 10 42") => (1 10 42)

Example — re.sub with a function (Python re.sub(p, lambda m: ..., s)):

;;; version 0.1.0  2026-05-12
;; Wrap every word with <b>...</b>.
(ppcre:regex-replace-all
 "\\w+"
 "hello world"
 (lambda (match-start match-end &rest _)
   (declare (ignore _))
   (format nil "<b>~A</b>"
           (subseq "hello world" match-start match-end))))
;; => "<b>hello</b> <b>world</b>"

Backslashes: Common Lisp strings only know \\ (literal backslash) and a few char escapes. So a regex like Python's r"\d+" becomes the CL string "\\d+". There's no raw-string syntax in standard CL; third-party reader macros (cl-interpol, pythonic-string-reader) add one.


File I/O

Common Lisp's stream / pathname / file model is broadly similar to Python's: open a stream, read or write, close (preferably via a scoped macro).

The canonical scoped open is WITH-OPEN-FILE — analogous to Python's with open(...) as f:.

Reference: CLHS — with-open-file.

Reading

Python:

def read_file_lines(filename):
    with open(filename, "r") as infile:
        return infile.readlines()

for line in read_file_lines("somefile.log"):
    print(line.rstrip("\n"))

Common Lisp (idiomatic, ANSI only):

;;;; file-io-example.lisp  --- version 0.1.0  2026-05-12

(defun read-file-lines (filename)
  "Return the lines of FILENAME as a list of strings, in order."
  (with-open-file (in filename :direction :input
                                :if-does-not-exist :error)
    (loop for line = (read-line in nil nil)
          while line
          collect line)))

(dolist (line (read-file-lines "somefile.log"))
  (format t "~A~%" line))

With UIOP (much shorter, recommended for short files):

;; UIOP ships with ASDF, no extra Quicklisp install needed.
;; https://asdf.common-lisp.dev/uiop.html

(uiop:read-file-lines "somefile.log")   ; => list of strings, no trailing \n
(uiop:read-file-string "somefile.log")  ; => whole contents as one string
Python Common Lisp (ANSI) Common Lisp (UIOP)
open(fn).read() (with-open-file (s fn) (let ((buf (make-string (file-length s)))) (read-sequence buf s) buf)) (uiop:read-file-string fn)
open(fn).readlines() See read-file-lines above (uiop:read-file-lines fn)
for line in open(fn): … (with-open-file (in fn) (loop for line = (read-line in nil nil) while line do …))
open(fn, "rb").read() (with-open-file (s fn :element-type '(unsigned-byte 8)) (let ((buf (make-array (file-length s) :element-type '(unsigned-byte 8)))) (read-sequence buf s) buf))

Writing

Python:

def write_strings_to_file(strings, filename):
    with open(filename, "w") as f:
        for s in strings:
            f.write(s + "\n")

Common Lisp:

;;;; version 0.1.0  2026-05-12

(defun write-strings-to-file (strings filename)
  "Write each element of STRINGS to FILENAME on its own line."
  (with-open-file (out filename
                       :direction :output
                       :if-exists :supersede
                       :if-does-not-exist :create)
    (dolist (s strings)
      (write-line s out))))

;; Or, via UIOP:
;; (with-open-file (out filename :direction :output :if-exists :supersede)
;;   (format out "~{~A~%~}" strings))
Python Common Lisp Notes
open(fn, "w") :direction :output :if-exists :supersede :if-does-not-exist :create
open(fn, "a") :direction :output :if-exists :append :if-does-not-exist :create
open(fn, "x") :direction :output :if-exists :error :if-does-not-exist :create
f.write(s) (write-string s stream), (write-line s stream), (format stream "..." ...)
print(...) to file (format stream "...~%" ...) ~% is newline.

Paths

CL has its own pathname object; for most modern code, UIOP is the portable interface. Reference: UIOP — pathname utilities.

Python pathlib / os.path Common Lisp (UIOP)
Path("/a/b/c.txt").name (file-namestring "/a/b/c.txt")"c.txt"
Path("/a/b/c.txt").stem (pathname-name "/a/b/c.txt")"c"
Path("/a/b/c.txt").suffix (pathname-type "/a/b/c.txt")"txt"
Path("/a/b/c.txt").parent (uiop:pathname-directory-pathname "/a/b/c.txt")
os.path.join("a", "b") (uiop:merge-pathnames* "b/" "a/") (directories must end in /)
os.path.exists(p) (probe-file p) (returns the truename or nil); also (uiop:file-exists-p p) / (uiop:directory-exists-p p)
os.path.abspath(p) (truename p), (uiop:ensure-absolute-pathname p)
os.listdir(d) (uiop:directory-files d) + (uiop:subdirectories d)
os.makedirs(p, exist_ok=True) (ensure-directories-exist p)
os.remove(p) (delete-file p)
os.rename(a, b) (rename-file a b)

Numbers — Brief Notes

CL has a rich numeric tower (integer, ratio, float, complex) and exact integer arithmetic is unlimited-precision by default — no int vs long split.

Python Common Lisp
1 / 20.5 (/ 1 2)1/2 (exact rational); (float (/ 1 2))0.5
1 // 2 (floor 1 2)0, second value 1 (remainder)
1 % 2 (mod 1 2) (positive result for positive divisor), (rem 1 2) (C-style)
divmod(a, b) (floor a b) — returns both quotient and remainder as multiple values
2 ** 10 (expt 2 10)
abs(x) (abs x)
round(x) (round x) — banker's rounding; for "round half away from zero" use a custom fn
math.floor / ceil / trunc / round floor, ceiling, truncate, round — all return two values: integer result and remainder
float('inf') sb-ext:double-float-positive-infinity (SBCL), implementation-specific in general
math.isnan(x) (/= x x) (NaN != itself); SBCL: (sb-ext:float-nan-p x)
random.random() (random 1.0)
random.randint(a, b) (inclusive) (+ a (random (- b a -1)))
random.seed(s) (setf *random-state* (sb-ext:seed-random-state s)) (SBCL); or save/restore *random-state*

Quick Reference: Library Loading

;;;; deps.lisp  --- version 0.1.0  2026-05-12

;; Quicklisp one-shot install (if not loaded):
;; (load "~/quicklisp/setup.lisp")  ; or your equivalent

(ql:quickload '(#:alexandria #:serapeum #:cl-ppcre))
;; UIOP is bundled with ASDF and already loaded.

Things Without Direct Translation

A few Python features have no clean one-liner counterpart in CL:

  • Generators / yield. No built-in lazy iteration. Use closures returning successive values, or cl-cont (continuations), or the iterate library's gathering, or build a list and mapc it.
  • with for arbitrary context managers. CL uses macros named with-* per resource (with-open-file, with-output-to-string, with-slots, with-hash-table-iterator, ...). The pattern is the same; the syntax is not generic.
  • Decorators. Use defmethod combinators (:before, :after, :around), or wrap functions with (setf (symbol-function 'foo) ...), or use Serapeum's def/local.
  • async / await. Outside the standard. Look at bordeaux-threads, lparallel, or implementation-specific concurrency.

License

This document follows the spirit of the elisp-for-python original (CC BY 4.0). Attribution to Charles Y. Choi for the structure.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment