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
- 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 overlist,vector, andstring. 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.
appendto grow a list), it is noted. - This is a draft; corrections welcome.
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:).
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.
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.
- string is a specialised vector of characters.
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
| 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. |
| 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) |
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. |
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) |
| 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-duplicatesgotcha: 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 tto keep the first occurrence, which is what most Python programmers expect. (CLHS —remove-duplicates)
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.
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
gethashreturns two values:nilis a perfectly valid value in a hash table, so a single return value can't distinguish "absent" from "present, butnil". Always use the second value (present-p) to check membership, never the first.
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)) |
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 |
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))[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[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. Onlynilis 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).
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) ; => 145for 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 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.
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))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
| 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).
| 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.
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'sr"\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.
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.
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)) |
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. |
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) |
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 / 2 → 0.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* |
;;;; 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.A few Python features have no clean one-liner counterpart in CL:
- Generators /
yield. No built-in lazy iteration. Use closures returning successive values, orcl-cont(continuations), or theiteratelibrary'sgathering, or build a list andmapcit. withfor arbitrary context managers. CL uses macros namedwith-*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
defmethodcombinators (:before,:after,:around), or wrap functions with(setf (symbol-function 'foo) ...), or use Serapeum'sdef/local. async/await. Outside the standard. Look atbordeaux-threads,lparallel, or implementation-specific concurrency.
This document follows the spirit of the elisp-for-python original (CC BY 4.0). Attribution to Charles Y. Choi for the structure.