Skip to main content

Command Palette

Search for a command to run...

Python to Basilisp: Clojure's Brain in Python's Body

Updated
19 min read

title: "Python to Basilisp: Clojure's Brain in Python's Body" published: true description: "A comprehensive comparison of Python and Basilisp — the Clojure dialect that compiles to Python bytecode. Immutable data structures, macros, and the entire PyPI ecosystem, side by side with every topic in the official Python tutorial." tags: python, clojure, functional, beginners

cover_image:

Python to Basilisp: Clojure's Brain in Python's Body

What if you could have Clojure's immutable data structures, its elegant sequence abstractions, its macros and protocols — but running on the Python VM with full access to PyPI?

That's Basilisp. It's a Clojure-compatible Lisp dialect that compiles to Python 3 bytecode. Not a bridge, not a transpiler with gaps — Basilisp code becomes Python bytecode and runs on CPython, with seamless interop in both directions. You can import NumPy into your Basilisp code or require a Basilisp namespace from Python.

If Hy is "Python wearing Lisp syntax," Basilisp is "Clojure wearing Python's shoes." The syntax comes from Clojure. The semantics — immutability, persistent data structures, atoms, protocols, transducers — come from Clojure. But the runtime, the libraries, and the deployment story are all Python.

This article walks through every major topic in the official Python tutorial and shows how Basilisp handles the same concept. If you know Python, you're already halfway there. If you know Clojure, you're home.


1. Whetting Your Appetite

Python sells itself on readability, rapid prototyping, and the largest package ecosystem in the world.

Basilisp inherits Python's ecosystem — literally, via pip install — while adding what Python lacks:

  1. Immutable data structures by default, backed by efficient persistent implementations
  2. First-class sequence abstractions that unify iteration across every data type
  3. Macros for compile-time code generation
  4. Atoms for safe, lock-free concurrent state
  5. Protocols and multimethods for polymorphism without inheritance

Install with pip install basilisp and type basilisp repl. You're in.


2. The Interpreter / The REPL

Python:

>>> 2 + 2
4
>>> print("Hello, world!")
Hello, world!

Basilisp:

basilisp.user=> (+ 2 2)
4
basilisp.user=> (println "Hello, world!")
Hello, world!
nil

The REPL is central to Basilisp's workflow, just as it is in Clojure. Every expression returns a value — println prints the string and returns nil (Basilisp's equivalent of Python's None).

The parentheses aren't decoration — they're the syntax. The first element of every list is the function; the rest are arguments. This uniformity is what makes macros possible.


3. An Informal Introduction

Numbers

Python:

>>> 17 / 3       # 5.666...
>>> 17 // 3      # 5
>>> 17 % 3       # 2
>>> 5 ** 2       # 25

Basilisp:

(/ 17 3)         ;=> 17/3  (a Fraction — exact!)
(quot 17 3)      ;=> 5
(rem 17 3)       ;=> 2
(Math/pow 5 2)   ;=> 25.0
;; or simply:
(* 5 5)          ;=> 25

The first surprise: (/ 17 3) returns 17/3 — a Python fractions.Fraction object. No precision lost. Basilisp inherits Clojure's preference for exact arithmetic by default.

Operators accept multiple arguments naturally:

(+ 1 2 3 4 5)     ;=> 15
(* 2 3 4)          ;=> 24
(< 1 2 3 4)        ;=> true (chained comparison)

Python integers are already arbitrary-precision, and Basilisp inherits that:

(* (range 1 100))  ;; not quite — but you get the idea
(reduce * (range 1N 21N))  ;=> 2432902008176640000

Strings

Python:

word = "Python"
word[0]        # 'P'
word[0:2]      # 'Py'
len(word)      # 6
f"Hello, {word}!"

Basilisp:

(def word "Basilisp")
(get word 0)          ;=> "B"
(subs word 0 2)       ;=> "Ba"
(count word)          ;=> 8
(str "Hello, " word "!")  ;=> "Hello, Basilisp!"

Strings in Basilisp are native Python strings. str concatenates, subs slices, and count gives the length. For formatted output, use Clojure-style format:

(format "Hello, %s! You have %d messages." word 42)
;=> "Hello, Basilisp! You have 42 messages."

Lists and Vectors

Python:

squares = [1, 4, 9, 16, 25]
squares[0]              # 1
squares + [36, 49]      # [1, 4, 9, 16, 25, 36, 49]
squares.append(36)      # mutates!

Basilisp:

(def squares [1 4 9 16 25])
(get squares 0)              ;=> 1
(squares 0)                  ;=> 1 (vectors are functions!)
(conj squares 36)            ;=> [1 4 9 16 25 36]
(into squares [36 49])       ;=> [1 4 9 16 25 36 49]
squares                      ;=> [1 4 9 16 25] (unchanged!)

This is the fundamental shift. Basilisp vectors are persistent and immutable. conj doesn't modify squares — it returns a new vector that shares structure with the old one. No defensive copying, no "who mutated my list" bugs, no thread-safety concerns.

Vectors can also act as functions of their index — (squares 0) is equivalent to (get squares 0).


4. Control Flow

if / cond

Python:

if x < 0:
    print("Negative")
elif x == 0:
    print("Zero")
else:
    print("Positive")

Basilisp:

(cond
  (neg? x) (println "Negative")
  (zero? x) (println "Zero")
  :else (println "Positive"))

if is a simple two-branch expression:

(def label (if (neg? x) "negative" "non-negative"))

No ternary operator needed — if already returns a value. Everything in Basilisp is an expression.

for loops

Python:

for word in ["cat", "window", "defenestrate"]:
    print(word, len(word))

Basilisp:

(doseq [word ["cat" "window" "defenestrate"]]
  (println word (count word)))

doseq is for side effects (like printing). When you want to transform data — which is most of the time — use map, filter, or for:

(for [word ["cat" "window" "defenestrate"]]
  [word (count word)])
;=> (["cat" 3] ["window" 6] ["defenestrate" 13])

Basilisp's for is a list comprehension, not a loop. It returns a lazy sequence.

range

Python:

list(range(0, 10, 2))  # [0, 2, 4, 6, 8]

Basilisp:

(range 0 10 2)  ;=> (0 2 4 6 8)

Basilisp's range is lazy — it generates values on demand without allocating memory for all of them.

while / loop-recur

Python:

a, b = 0, 1
while a < 10:
    print(a)
    a, b = b, a + b

Basilisp:

(loop [a 0 b 1]
  (when (< a 10)
    (println a)
    (recur b (+ a b))))

loop/recur is Basilisp's structured iteration. recur jumps back to the loop head with new bindings — it's tail-call optimized with zero stack growth. No while True needed.

Pattern Matching

Python 3.10+:

match command:
    case "quit":
        quit_game()
    case "help":
        show_help()
    case _:
        print("Unknown")

Basilisp:

(case command
  "quit" (quit-game)
  "help" (show-help)
  (println "Unknown"))

case handles constant dispatch. For more complex pattern matching, cond with predicates is idiomatic:

(cond
  (string? x)  (println "It's a string")
  (vector? x)  (println "It's a vector")
  :else        (println "Something else"))

Functions

Python:

def fib(n):
    """Return Fibonacci series up to n."""
    result = []
    a, b = 0, 1
    while a < n:
        result.append(a)
        a, b = b, a + b
    return result

Basilisp:

(defn fib
  "Return Fibonacci series up to n."
  [n]
  (->> [0 1]
       (iterate (fn [[a b]] [b (+ a b)]))
       (map first)
       (take-while #(< % n))))

The Basilisp version reads as a pipeline: start with [0 1], generate successive pairs, extract the first element, and keep going while less than n. No mutation, no accumulator, no while loop. Data flows through transformations.

The ->> "thread-last" macro turns nested calls into a readable pipeline — one of Clojure's most beloved features.

Default Arguments and Keyword Arguments

Python:

def greet(name, greeting="Hello"):
    print(f"{greeting}, {name}!")

def log(*args, **kwargs):
    print(args, kwargs)

Basilisp:

(defn greet
  ([name] (greet name "Hello"))
  ([name greeting] (println (str greeting ", " name "!"))))

(defn log [& args]
  (println args))

Basilisp uses multi-arity functions for defaults: calling (greet "Ada") dispatches to the one-arg version, which calls the two-arg version with "Hello".

For keyword arguments, Basilisp supports map destructuring:

(defn create-user [& {:keys [name email role] :or {role "member"}}]
  {:name name :email email :role role})

(create-user :name "Ada" :email "ada@example.com")
;=> {:name "Ada" :email "ada@example.com" :role "member"}

Lambda Expressions

Python:

double = lambda x: x * 2
sorted(pairs, key=lambda p: p[1])

Basilisp:

(def double #(* % 2))         ; reader macro shorthand
(sort-by second pairs)        ; or: (sort-by #(nth % 1) pairs)

#(* % 2) is shorthand for (fn [x] (* x 2)). % is the argument, %2 the second argument, etc. No restriction to a single expression — fn bodies can be as complex as needed.


5. Data Structures

The Full Collection Family

PythonBasilispProperties
[1, 2, 3][1 2 3]Vector: immutable, indexed, O(log32 n) access
(1, 2, 3)N/A (vectors cover this)Vectors are already immutable
{"a": 1}{"a" 1} or {:a 1}Map: immutable, O(log n) lookup
{1, 2, 3}#{1 2 3}Set: immutable, O(1) membership
N/A'(1 2 3)List: immutable linked list

Every collection is immutable and persistent. "Persistent" means new versions share structure with old ones — adding to a million-element vector doesn't copy a million elements.

Need a mutable Python list for interop? Use the #py prefix:

(def py-list #py [1 2 3])     ; native Python list
(def py-dict #py {"a" 1})     ; native Python dict
(def py-set #py #{1 2 3})     ; native Python set

Maps (Dictionaries)

Python:

tel = {"jack": 4098, "sape": 4139}
tel["guido"] = 4127
del tel["sape"]
{x: x**2 for x in range(5)}

Basilisp:

(def tel {:jack 4098 :sape 4139})
(assoc tel :guido 4127)       ;=> {:jack 4098 :sape 4139 :guido 4127}
(dissoc tel :sape)            ;=> {:jack 4098}
(into {} (map (fn [x] [x (* x x)]) (range 5)))

Keywords (:jack, :sape) are idiomatic Basilisp map keys. They double as accessor functions:

(:jack tel)       ;=> 4098
(:missing tel 0)  ;=> 0 (default value)

assoc and dissoc return new maps. The original is never modified.

Sets

Python:

a = {1, 2, 3, 4}
b = {3, 4, 5, 6}
a - b            # {1, 2}
a | b            # {1, 2, 3, 4, 5, 6}
a & b            # {3, 4}

Basilisp:

(require '[basilisp.set :as set])
(def a #{1 2 3 4})
(def b #{3 4 5 6})
(set/difference a b)     ;=> #{1 2}
(set/union a b)          ;=> #{1 2 3 4 5 6}
(set/intersection a b)   ;=> #{3 4}

Sets are also functions — (a 3) returns 3 (truthy), (a 7) returns nil (falsy). Handy for filtering:

(filter #{3 4 5} (range 10))  ;=> (3 4 5)

List Comprehensions

Python:

[x**2 for x in range(10) if x % 2 == 0]

Basilisp:

(for [x (range 10) :when (even? x)]
  (* x x))
;=> (0 4 16 36 64)

for supports :when for filtering, :let for local bindings, and multiple binding forms for nested iteration — all lazily evaluated.

Destructuring

Basilisp's destructuring is more powerful than Python's unpacking:

;; Sequential
(let [[a b c & rest] [1 2 3 4 5 6]]
  [a b c rest])
;=> [1 2 3 (4 5 6)]

;; Associative
(let [{:keys [name email] :or {email "N/A"}} {:name "Ada"}]
  [name email])
;=> ["Ada" "N/A"]

;; Nested
(let [[a [b c]] [1 [2 3]]]
  [a b c])
;=> [1 2 3]

This works everywhere — let, fn parameters, for bindings, loop bindings.

Looping Techniques

Python:

for i, v in enumerate(["tic", "tac", "toe"]):
    print(i, v)

for q, a in zip(questions, answers):
    print(q, a)

for key, value in knights.items():
    print(key, value)

Basilisp:

(doseq [[i v] (map-indexed vector ["tic" "tac" "toe"])]
  (println i v))

(doseq [[q a] (map vector questions answers)]
  (println q a))

(doseq [[key value] knights]   ; maps are sequences of [k v] pairs
  (println key value))

6. Modules and Namespaces

Importing Python

Python:

import math
from os.path import join, exists
import numpy as np

Basilisp:

(import math)
(import [os.path :as path])
(import [numpy :as np])

Or in the ns declaration (the idiomatic way):

(ns myapp.core
  (:import math
           [os.path :as path]
           [numpy :as np]))

Requiring Basilisp Namespaces

(ns myapp.core
  (:require [basilisp.string :as str]
            [basilisp.set :as set]
            [myapp.utils :refer [helper-fn]]))

This mirrors Clojure's ns macro exactly. Basilisp files use the .lpy extension and map to namespaces by their directory path.

Python Builtins

All Python builtins are available under the python namespace without imports:

(python/abs -42)          ;=> 42
(python/len #py [1 2 3])  ;=> 3
(python/type 42)          ;=> <class 'int'>

The Script Entry Point

Python:

if __name__ == "__main__":
    main()

Basilisp scripts can use a shebang:

#!/usr/bin/env basilisp run

(defn -main [& args]
  (println "Hello from the command line!"))

7. Input and Output

String Formatting

Python:

name = "World"
f"Hello, {name}!"
"{:.2f}".format(3.14159)

Basilisp:

(def name "World")
(str "Hello, " name "!")           ;=> "Hello, World!"
(format "Hello, %s!" name)         ;=> "Hello, World!"
(format "%.2f" 3.14159)            ;=> "3.14"

str concatenates, format uses printf-style directives. For complex output, basilisp.pprint/pprint pretty-prints data structures.

File I/O

Python:

with open("data.txt", encoding="utf-8") as f:
    content = f.read()

with open("output.txt", "w", encoding="utf-8") as f:
    f.write("Hello\n")

Basilisp:

(def content (slurp "data.txt"))

(spit "output.txt" "Hello\n")

slurp and spit — inherited from Clojure, arguably the best-named I/O functions in any language. For line-by-line processing:

(with-open [rdr (basilisp.io/reader "data.txt")]
  (doseq [line (line-seq rdr)]
    (println line)))

with-open mirrors Python's with statement — it ensures the resource is closed when the block exits.

JSON

Python:

import json
data = json.loads('{"name": "Ada"}')
json.dumps(data)

Basilisp:

(require '[basilisp.json :as json])
(def data (json/read-str "{\"name\": \"Ada\"}" ** :key-fn keyword))
;=> {:name "Ada"}
(json/write-str data)
;=> "{\"name\":\"Ada\"}"

The :key-fn keyword option keywordizes string keys — JSON becomes idiomatic Basilisp data instantly.


8. Errors and Exceptions

Try / Catch

Python:

try:
    x = int(input("Number: "))
except ValueError as e:
    print(f"Invalid: {e}")
else:
    print(f"Got: {x}")
finally:
    print("Done")

Basilisp:

(try
  (let [x (python/int (python/input "Number: "))]
    (println "Got:" x))
  (catch python/ValueError e
    (println "Invalid:" (str e)))
  (finally
    (println "Done")))

Since Basilisp runs on Python, it catches Python exceptions directly. python/ValueError, python/TypeError, python/IOError — they're all available.

Raising Exceptions

Python:

raise ValueError("something went wrong")
raise RuntimeError("failed") from original_error

Basilisp:

(throw (python/ValueError "something went wrong"))
(throw (python/RuntimeError "failed") original-error)

throw accepts an optional second argument for exception chaining, matching Python's raise ... from ....

Custom Exceptions

Python:

class InsufficientFunds(Exception):
    def __init__(self, balance, amount):
        self.balance = balance
        self.amount = amount
        super().__init__(f"Need {amount}, have {balance}")

Basilisp — you can define a Python class, or use data-oriented error handling with ex-info:

(throw (ex-info "Insufficient funds"
         {:type :insufficient-funds
          :balance 100
          :amount 150}))

(try
  (do-transaction)
  (catch python/Exception e
    (when-let [data (ex-data e)]
      (println "Balance:" (:balance data))
      (println "Needed:" (:amount data)))))

ex-info creates an exception carrying a data map — Clojure's "data over classes" philosophy. One generic mechanism, infinite data shapes.


9. Classes and Objects

Python's Object-Oriented Approach

class Dog:
    kind = "canine"

    def __init__(self, name):
        self.name = name

    def speak(self):
        return f"{self.name} says woof!"

rex = Dog("Rex")
rex.speak()

Basilisp's Data-Oriented Approach

The simplest equivalent — just a map and a function:

(def rex {:kind "canine" :name "Rex"})

(defn speak [dog]
  (str (:name dog) " says woof!"))

(speak rex)  ;=> "Rex says woof!"

Protocols (Interfaces)

For polymorphism without inheritance, use protocols:

(defprotocol Speakable
  (speak [this]))

(defrecord Dog [name]
  Speakable
  (speak [this] (str name " says woof!")))

(defrecord Cat [name]
  Speakable
  (speak [this] (str name " says meow!")))

(speak (->Dog "Rex"))      ;=> "Rex says woof!"
(speak (->Cat "Whiskers")) ;=> "Whiskers says meow!"

Records implement the map interface, so :name still works as an accessor:

(def rex (->Dog "Rex"))
(:name rex)  ;=> "Rex"
(assoc rex :name "Spot")  ;=> #user.Dog{:name "Spot"}

Multimethods

For dispatch on any function of the arguments (not just type):

(defmulti encounter (fn [a b] [(:species a) (:species b)]))

(defmethod encounter [:dog :cat] [a b]
  (str (:name a) " chases " (:name b)))

(defmethod encounter [:cat :dog] [a b]
  (str (:name a) " hisses at " (:name b)))

(defmethod encounter :default [a b]
  (str (:name a) " ignores " (:name b)))

(encounter {:species :dog :name "Rex"}
           {:species :cat :name "Whiskers"})
;=> "Rex chases Whiskers"

Python Interop for Real Classes

Need to extend a Python class? Use deftype or proxy:

(import [http.server :refer [BaseHTTPRequestHandler]])

(deftype MyHandler [request client-address server]
  ^:abstract BaseHTTPRequestHandler
  (do-GET [self]
    (.send-response self 200)
    (.end-headers self)
    (.write (.-wfile self) (.encode "Hello from Basilisp!"))))

Iterators and Generators

Python:

def countdown(n):
    while n > 0:
        yield n
        n -= 1

Basilisp:

;; Lazy sequence (idiomatic)
(defn countdown [n]
  (when (pos? n)
    (lazy-seq (cons n (countdown (dec n))))))

(countdown 5)  ;=> (5 4 3 2 1)

Or using Python-style generators via yield:

(defn countdown [n]
  (loop [i n]
    (when (pos? i)
      (yield i)
      (recur (dec i)))))

Most of the time, lazy sequences are more idiomatic than generators in Basilisp.

Generator Expressions / Transducers

Python:

sum(x*x for x in range(10))

Basilisp:

;; Simple
(reduce + (map #(* % %) (range 10)))

;; With threading macro
(->> (range 10) (map #(* % %)) (reduce +))

;; With transducers (most efficient)
(transduce (map #(* % %)) + (range 10))

Transducers are Basilisp's power tool for data pipelines. They compose transformations without creating intermediate sequences:

(def xform
  (comp
    (filter even?)
    (map #(* % %))
    (take 5)))

(into [] xform (range 100))
;=> [0 4 16 36 64]

10. The Standard Library

Python's Ecosystem, Basilisp's Abstractions

Basilisp has two standard libraries: its own Clojure-ported core, and all of Python's.

Basilisp core libraries:

LibraryPurpose
basilisp.stringString manipulation (split, join, trim, replace...)
basilisp.setSet operations (union, intersection, difference...)
basilisp.ioI/O (reader, writer, resource...)
basilisp.jsonJSON read/write
basilisp.ednEDN (Extensible Data Notation) read/write
basilisp.pprintPretty-printing
basilisp.shellShell command execution
basilisp.testTesting framework
basilisp.walkRecursive data structure walking
basilisp.stacktraceStack trace formatting

Plus every Python module:

;; OS and files
(import os)
(os/listdir ".")

;; Regex (Basilisp has built-in support too)
(re-find #"\d+" "hello 42 world")  ;=> "42"
(re-seq #"\d+" "hello 42 world 99")  ;=> ("42" "99")

;; Math
(import math)
(math/cos (/ math/pi 4))

;; Dates
(import [datetime :refer [date]])
(def now (date/today))
(.strftime now "%Y-%m-%d")

;; HTTP
(import [urllib.request :as req])
(slurp (req/urlopen "https://httpbin.org/get"))

Third-Party Libraries — The Killer Feature

;; NumPy
(import [numpy :as np])
(def arr (np/array [1 2 3 4 5]))
(np/mean arr)  ;=> 3.0

;; Requests
(import requests)
(def r (requests/get "https://api.github.com"))
(.-status-code r)  ;=> 200

;; Flask
(import [flask :refer [Flask]])
(def app (Flask __name__))

If it's on PyPI, it works in Basilisp. pip install it and import it. This is the reason Basilisp exists — Clojure semantics with Python's ecosystem.


11. Advanced Standard Library

Output Formatting

(require '[basilisp.pprint :refer [pprint]])
(pprint {:name "Ada"
         :languages ["Python" "Basilisp" "Clojure"]
         :scores {:math 98 :cs 100}})

Concurrency: Atoms

Where Python requires locks for shared state, Basilisp uses atoms:

(def counter (atom 0))
(swap! counter inc)          ;=> 1
(swap! counter + 10)         ;=> 11
@counter                     ;=> 11 (deref)
(reset! counter 0)           ;=> 0

swap! is atomic — it retries if another thread modified the atom between read and write. No locks, no deadlocks, no race conditions. The update function must be pure (free of side effects).

Add validators and watchers:

(set-validator! counter #(>= % 0))  ; must be non-negative
(add-watch counter :log
  (fn [_ _ old new]
    (println "Changed from" old "to" new)))

Futures and Parallel Map

(def f (future (expensive-computation)))
@f  ;=> result (blocks until ready)

(pmap expensive-fn large-collection)  ; parallel map

Transducers (Continued)

;; Process a CSV: extract prices, filter positives, sum them
(def xform
  (comp
    (map :price)
    (keep identity)
    (filter pos?)))

(transduce xform + 0 data)

Transducers are reusable, composable, and work with any data source — sequences, channels, or custom reducers.

Exact Arithmetic

(+ 1/10 2/10)      ;=> 3/10  (exact Fraction)
(= (+ 1/10 2/10) 3/10)  ;=> true
(* 0.1M 0.2M)      ;=> 0.02M (Decimal)

M suffix creates decimal.Decimal values, ratios use / syntax for fractions.Fraction.


12. Virtual Environments and Packages

Basilisp uses Python's packaging ecosystem directly:

python -m venv myenv
source myenv/bin/activate
pip install basilisp
pip install requests flask numpy

Project structure:

myproject/
  src/myproject/core.lpy
  tests/myproject/test_core.lpy
  pyproject.toml

Basilisp files use .lpy and live alongside Python files. A project can freely mix .py and .lpy — Python calls Basilisp, Basilisp calls Python.

For bootstrapping Basilisp from Python:

# __init__.py
import basilisp.main
basilisp.main.init()

After this, import myproject.core will find and compile .lpy files automatically.


13. Floating-Point Arithmetic

Same hardware, same IEEE 754, same surprise:

(= (+ 0.1 0.2) 0.3)  ;=> false

But Basilisp offers two escape hatches:

;; Exact rationals (Fraction)
(= (+ 1/10 2/10) 3/10)  ;=> true

;; Exact decimals
(= (+ 0.1M 0.2M) 0.3M)  ;=> true

Both are literal syntax — no imports, no constructors. Just 1/10 or 0.1M.


14. The REPL Experience

The Basilisp REPL supports:

  • History and line editing
  • Syntax highlighting (with pip install basilisp[pygments])
  • nREPL server for editor integration (Emacs CIDER, VS Code Calva, etc.)
  • The *1, *2, *3 special vars hold the last three results
  • *e holds the last exception
basilisp.user=> (+ 1 2)
3
basilisp.user=> (* *1 10)
30

Editor integration via nREPL means you can evaluate expressions inline, inspect data, and navigate documentation without leaving your editor.


15. The Secret Weapon: Macros

Python doesn't have macros. Basilisp does, and they're hygienic.

Example: A Timing Macro

(defmacro time-it [& body]
  `(let [start# (python/time.time)]
     ~@body
     (println "Elapsed:" (- (python/time.time) start#) "seconds")))

(time-it
  (reduce + (range 1000000)))

The # suffix generates unique symbols to prevent variable capture. The backtick ` quotes the template, ~ unquotes values, and ~@ splices sequences.

Example: A Retry Macro

(defmacro with-retry [n & body]
  `(loop [attempts# ~n]
     (let [result# (try
                     {:ok (do ~@body)}
                     (catch python/Exception e#
                       (if (pos? attempts#)
                         {:retry true}
                         (throw e#))))]
       (if (:retry result#)
         (recur (dec attempts#))
         (:ok result#)))))

(with-retry 3
  (fetch-flaky-api))

In Python, you'd write a decorator or a wrapper function with callbacks. In Basilisp, the macro generates exactly the code you need, with zero runtime overhead. The caller's code looks like a native language feature.

Example: Thread-safe Logging

(defmacro log [level & args]
  `(when (>= ~level @log-level)
     (locking log-lock
       (println (str "[" ~level "]") ~@args))))

Macros let you build domain-specific languages embedded directly in Basilisp. This is the deep reason Lisp has survived for 65+ years.


The Big Picture

DimensionPythonBasilisp
RuntimeCPython bytecodeCPython bytecode (same!)
SyntaxIndentation, keywordsS-expressions, minimal
ParadigmMulti-paradigm (imperative-first)Functional-first (with escape hatches)
MutabilityMutable by defaultImmutable by default
Data StructuresLists, dicts, sets (mutable)Vectors, maps, sets (persistent, immutable)
ConcurrencyGIL, locks, async/awaitAtoms, futures, pmap
PolymorphismClasses + inheritanceProtocols + multimethods
MetaprogrammingDecorators, metaclassesMacros (compile-time code generation)
LibrariesPyPIPyPI (same!) + Basilisp core
Type SystemDynamic, gradual typingDynamic
SequencesIterators, generatorsLazy seqs, transducers
REPLBasicnREPL, editor integration

Closing Thoughts

Basilisp occupies a remarkable niche. It brings Clojure's most powerful ideas — immutable data, sequence abstractions, protocols, atoms, macros — to the Python ecosystem. Every PyPI library works. Every Python tool applies. But the code you write is fundamentally different: data that can't change, functions that compose, and a language that reshapes itself to fit your problem.

If you've ever wished Python's data structures were immutable. If you've ever written a pipeline of map/filter/reduce and thought "this should be more natural." If you've ever fought a race condition that an atom would have prevented. Basilisp might be the language that makes you rethink how you write Python.

You don't have to migrate. Start with one .lpy file in your existing Python project. Import your Python code. See how it feels. The parentheses will be strange for an hour. The immutability will be strange for a day. Then you'll wonder how you ever lived without them.

(println "Welcome to Basilisp.")
(println "Where Python meets Clojure,")
(println "and data stays exactly as you left it.")

Get started: pip install basilisp && basilisp repl.


This article was written as a companion to the official Python tutorial. Every section maps to a chapter in that tutorial, showing the same concepts through Basilisp's Clojure-on-Python lens. For more, visit docs.basilisp.org.