What Homoiconicity Means
Homoiconicity means a language's source code is represented using the same data structures the language itself manipulates. In LISP, both code and data are built from S-expressions, parenthesized lists of symbols, numbers, and nested lists, so the expression (+ 1 2) is simultaneously 'a function call adding 1 and 2' when evaluated, and 'a three-element list' when treated as data. This dual nature is what lets LISP programs read, construct, and rewrite other LISP programs using the same list operations they'd use on any other data.
Cricket analogy: Homoiconicity is like a scorecard that is simultaneously the official record and the input to the next over's tactics — the same sheet of numbers a captain reads is also what the analyst feeds back into planning, no separate format needed.
S-Expressions as the Common Format
Because S-expressions are just nested lists, a LISP program can traverse another program's structure using ordinary list operations like car (first element) and cdr (rest of the list). Given the form (+ 1 2), (car '(+ 1 2)) returns the symbol + and (cdr '(+ 1 2)) returns the list (1 2), the same two primitives you'd use to walk any list of data. There is no separate 'abstract syntax tree' API to learn; the AST is the list structure you already know how to manipulate.
Cricket analogy: Walking code with car/cdr is like reading a scorecard's over-by-over list using the same 'first ball, remaining balls' logic you'd apply to any sequence, without needing a special scorecard-parsing tool.
;; Code is just nested lists — you can inspect and build it with list operations
(car '(+ 1 2)) ; => +
(cdr '(+ 1 2)) ; => (1 2)
;; Construct new code as data, then evaluate it
(defparameter *expr* (list '+ 1 2))
(eval *expr*) ; => 3
;; A macro-like helper built entirely from list manipulation
(defun negate-form (form)
(list 'not form))
(eval (negate-form '(> 2 3))) ; => TWhy This Enables Macros and Code Generation
Because programs are ordinary data, LISP programs can generate other programs at runtime or compile time, the foundation of macros, embedded domain-specific languages, and metaprogramming in general. A LISP program can build up an S-expression piece by piece with list, cons, and quasiquote, then hand it to eval or splice it into a macro expansion, blurring the line between 'writing code' and 'writing a program that writes code.' Languages without homoiconicity, like Java or Python, must instead expose a separate reflection or AST API (e.g. Python's ast module) to achieve anything comparable, and that API's shape rarely matches the language's own syntax.
Cricket analogy: This is like a team's analytics department not just reading match data but algorithmically generating new tactical playbooks from it — the playbook and the data share the same format, so one can produce the other directly.
Contrast this with a typical non-homoiconic language: to programmatically inspect or generate code in Python you reach for the separate ast module, and the resulting tree's shape looks nothing like ordinary Python data. In LISP there is no such gap — the list you'd build with cons and the code the reader parses are the same kind of object.
eval and Its Risks
eval takes a LISP data structure and evaluates it as code, the natural counterpart to homoiconicity, since code is data, you can always ask the interpreter to run some data as a program. This is genuinely useful for building interpreters, configuration systems, or REPL-driven tools, but it is also a security liability: calling eval on data that originated from an untrusted source (user input, a network request, a file you don't control) means that source can run arbitrary code with your program's full privileges. Production systems generally restrict eval to trusted, internally generated forms, or avoid it entirely in favor of macros resolved at compile time.
Cricket analogy: Using eval on untrusted data is like letting a random spectator's suggested tactic get automatically executed by the captain mid-over — trusting an unverified source with real, live authority over the match.
eval is a security-sensitive operation: never call eval on data that originates from an untrusted source (user input, network payloads, uploaded files). If you need to execute dynamic behavior safely, prefer a restricted interpreter, sandboxing, or macros expanded at compile time over raw eval on arbitrary input.
- Homoiconicity means LISP code and LISP data share the same representation: nested lists of symbols (S-expressions).
- car and cdr, the same primitives used on any list, are sufficient to inspect and traverse code structure — no separate AST API is needed.
- Programs can construct new code as data (with list, cons, quasiquote) and then run it with eval, or splice it into a macro expansion.
- This is the foundation of macros and metaprogramming in LISP, letting programs write programs.
- Non-homoiconic languages need a separate reflection/AST API (e.g. Python's ast module) whose shape doesn't match the language's own syntax.
- eval on untrusted input is a serious security risk and should be avoided or heavily sandboxed in production systems.
Practice what you learned
1. What does homoiconicity mean?
2. What do car and cdr return when applied to the form (+ 1 2)?
3. Why don't LISP programmers typically need a separate AST API to metaprogram, unlike in languages like Python?
4. What is the primary risk of calling eval on untrusted input?
5. What relationship does homoiconicity have to macros?
Was this page helpful?
You May Also Like
Macros in LISP
How LISP macros transform unevaluated code at compile time to create new syntax, and how to write them safely with defmacro, gensym, and hygiene practices.
quote and quasiquote
How LISP's quote, quasiquote, unquote, and unquote-splicing let you treat code as inert data and selectively rebuild it as templates.
Closures in LISP
How LISP closures bundle a function with its lexical environment to create private, persistent state, and common patterns and pitfalls that follow from it.
Related Reading
Related Study Notes in Programming
Browse all study notesApache Spark Study Notes
Programming · 30 topics
ProgrammingApache Flink Study Notes
Programming · 30 topics
ProgrammingHadoop Study Notes
Programming · 30 topics
ProgrammingSnowflake Study Notes
Programming · 30 topics
ProgrammingApache Airflow Study Notes
Programming · 30 topics
Programmingdbt (Data Build Tool) Study Notes
Programming · 30 topics