Build a Lisp Interpreter in Python

Inspired by Peter Norvig's classic *lis.py*. About 100 lines give you a working Scheme-like language.

What you just built: a reader (tokenize + parse), environments (lexical scope with outer links), closures (Procedure captures env), special forms (quote, if, define, set!, lambda, begin), and function application. That's the essential core of every Lisp.

# lisp.py
import math
import operator as op


def tokenize(source: str) -> list[str]:
    """Split source text into tokens: parentheses and atoms."""
    return source.replace('(', ' ( ').replace(')', ' ) ').split()


def parse(tokens: list[str]):
    """Read one expression from a token list (consuming tokens)."""
    if not tokens:
        raise SyntaxError('unexpected EOF')
    token = tokens.pop(0)
    if token == '(':
        lst = []
        while tokens and tokens[0] != ')':
            lst.append(parse(tokens))
        if not tokens:
            raise SyntaxError('missing )')
        tokens.pop(0)                  # discard ')'
        return lst
    if token == ')':
        raise SyntaxError('unexpected )')
    return atom(token)


def atom(token: str):
    """Numbers become int/float; everything else is a symbol (str)."""
    try:
        return int(token)
    except ValueError:
        try:
            return float(token)
        except ValueError:
            return token


class Env(dict):
    """An environment: a dict of {symbol: value} with an outer environment."""

    def __init__(self, params=(), args=(), outer=None):
        super().__init__(zip(params, args))
   
…
lisp> (define fact (lambda (n) (if (<= n 1) 1 (* n (fact (- n 1))))))
lisp> (fact 20)
2432902008176640000
lisp> (define make-adder (lambda (n) (lambda (x) (+ x n))))
lisp> ((make-adder 10) 5)
15
lisp> (map (lambda (x) (* x x)) (list 1 2 3 4))
(1 4 9 16)
lisp> (cons 0 (quote (1 2 3)))
(0 1 2 3)