DEV Community

kovan
kovan

Posted on

Python to Clojure: A Gentle Guide for Pythonistas

Python to Clojure: A Gentle Guide for Pythonistas

Python is the world's most popular general-purpose language. Clojure is a quiet powerhouse — a modern Lisp on the JVM that has earned fierce loyalty among developers who discover it. Both languages prize simplicity, but they mean very different things by the word.

This article walks through every major topic in the official Python tutorial and shows how Clojure approaches the same idea. Whether you're a Pythonista curious about functional programming or a polyglot looking for a side-by-side reference, this guide is for you.


1. Whetting Your Appetite

Python sells itself on readability, rapid prototyping, and "batteries included." It eliminates the compile-link cycle, encourages experimentation in the REPL, and runs everywhere.

Clojure shares the same REPL-driven culture, but takes a more opinionated stance: data is the universal interface. Where Python says "everything is an object," Clojure says "everything is data." Clojure runs on the JVM (and in the browser via ClojureScript), giving it access to the entire Java ecosystem — a different kind of "batteries included."

Both languages let you sit down and be productive in minutes. The difference is in what they optimize for: Python optimizes for readability of imperative code; Clojure optimizes for simplicity of data transformation.


2. The Interpreter / The REPL

Python has an interactive interpreter invoked with python:

>>> 2 + 2
4
>>> print("Hello, world!")
Hello, world!
Enter fullscreen mode Exit fullscreen mode

Clojure has the REPL (Read-Eval-Print Loop), which serves the same purpose but is even more central to the workflow. Most Clojure developers keep a REPL connected to their editor at all times:

user=> (+ 2 2)
4
user=> (println "Hello, world!")
Hello, world!
Enter fullscreen mode Exit fullscreen mode

The parentheses aren't noise — they're the syntax. Every expression is a list where the first element is the function. This uniformity is what makes Lisp macros possible, and it takes about a day to stop noticing the parens.


3. An Informal Introduction

Numbers

Python:

>>> 17 / 3      # 5.666...
>>> 17 // 3     # 5 (floor division)
>>> 17 % 3      # 2
>>> 5 ** 2      # 25
Enter fullscreen mode Exit fullscreen mode

Clojure:

(/ 17 3)       ;=> 17/3  (a rational! no precision lost)
(quot 17 3)    ;=> 5
(rem 17 3)     ;=> 2
(Math/pow 5 2) ;=> 25.0
Enter fullscreen mode Exit fullscreen mode

Right away, a philosophical difference appears. Clojure gives you a ratio (17/3) by default rather than silently losing precision. Clojure also has arbitrary-precision integers out of the box — no special int vs long distinction to worry about.

Strings

Python:

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

Clojure:

(def word "Clojure")
(get word 0)          ;=> \C  (a character)
(subs word 0 2)       ;=> "Cl"
(count word)          ;=> 7
(str "Hello, " word "!")
Enter fullscreen mode Exit fullscreen mode

Both treat strings as immutable. In Python, strings are sequences of characters; in Clojure, strings are Java strings, but you can also treat them as sequences of characters when needed via seq.

Lists

Python:

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

Clojure:

(def squares [1 4 9 16 25])
(get squares 0)              ;=> 1
(conj squares 36)            ;=> [1 4 9 16 25 36] (new vector!)
(into squares [36 49])       ;=> [1 4 9 16 25 36 49]
Enter fullscreen mode Exit fullscreen mode

This is the first big fork in the road. Python lists are mutable; Clojure vectors are persistent and immutable. conj doesn't change squares — it returns a new vector that efficiently shares structure with the old one. No defensive copying, no "did someone mutate my list?" bugs.


4. Control Flow

if / cond

Python:

if x < 0:
    print("Negative")
elif x == 0:
    print("Zero")
else:
    print("Positive")
Enter fullscreen mode Exit fullscreen mode

Clojure:

(cond
  (neg? x) (println "Negative")
  (zero? x) (println "Zero")
  :else (println "Positive"))
Enter fullscreen mode Exit fullscreen mode

In Clojure, if is an expression that returns a value (no need for a ternary operator). cond handles the multi-branch case.

for loops

Python:

for word in ["cat", "window", "defenestrate"]:
    print(word, len(word))
Enter fullscreen mode Exit fullscreen mode

Clojure:

(doseq [word ["cat" "window" "defenestrate"]]
  (println word (count word)))
Enter fullscreen mode Exit fullscreen mode

But here's the thing — doseq is for side effects. When you want to transform data (which is most of the time), you use map, filter, or for (which is a list comprehension, not a loop):

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

range

Python:

list(range(0, 10, 2))  # [0, 2, 4, 6, 8]
Enter fullscreen mode Exit fullscreen mode

Clojure:

(range 0 10 2)  ;=> (0 2 4 6 8)
Enter fullscreen mode Exit fullscreen mode

Clojure's range is lazy — it doesn't allocate memory for all elements upfront.

Pattern Matching

Python 3.10+:

match status:
    case 400:
        return "Bad request"
    case 401 | 403:
        return "Not allowed"
    case _:
        return "Something's wrong"
Enter fullscreen mode Exit fullscreen mode

Clojure (with core.match):

(match status
  400 "Bad request"
  (:or 401 403) "Not allowed"
  :else "Something's wrong")
Enter fullscreen mode Exit fullscreen mode

Clojure's pattern matching via core.match is a library, not built-in syntax — which is itself a philosophical statement. In Lisp, you can add any syntax you want via macros.

Functions

Python:

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

Clojure:

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

The Clojure version reads as a pipeline: start with [0 1], repeatedly produce the next pair, extract the first element, and keep going while it's less than n. No mutation, no accumulator variable, no while loop. Just data flowing through transformations.

Default Arguments, *args, **kwargs

Python:

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

def log(*args, **kwargs):
    print(args, kwargs)
Enter fullscreen mode Exit fullscreen mode

Clojure:

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

(defn log [& args]
  (println args))
Enter fullscreen mode Exit fullscreen mode

Clojure uses multi-arity functions for defaults and & args for variadics. For keyword arguments, the idiomatic approach is to pass a map:

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

(create-user {:name "Ada" :email "ada@example.com"})
Enter fullscreen mode Exit fullscreen mode

Lambda Expressions

Python:

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

Clojure:

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

In Clojure, anonymous functions are so common they have a shorthand: #(...) with % for the argument. Named functions and anonymous functions are treated identically — there's no second-class lambda.


5. Data Structures

Lists, Tuples, and Vectors

Python Clojure Properties
list[1, 2, 3] vector[1 2 3] Indexed, ordered
tuple(1, 2, 3) vector[1 2 3] (Clojure vectors are already immutable)
list'(1 2 3) Linked list, efficient head access

Python needs both lists (mutable) and tuples (immutable). Clojure needs only vectors (immutable, indexed) and lists (immutable, sequential). The mutability question simply doesn't arise.

Dictionaries / Maps

Python:

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

Clojure:

(def tel {"jack" 4098 "sape" 4139})
(assoc tel "guido" 4127)       ;=> new map with guido added
(dissoc tel "sape")            ;=> new map without sape
(into {} (map (fn [x] [x (* x x)]) (range 5)))
Enter fullscreen mode Exit fullscreen mode

Again: assoc and dissoc return new maps. The original is untouched. Clojure maps can also use keywords as keys (:jack instead of "jack"), which double as accessor functions — a small but delightful ergonomic touch:

(def person {:name "Ada" :age 36})
(:name person)  ;=> "Ada"
Enter fullscreen mode Exit fullscreen mode

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}
Enter fullscreen mode Exit fullscreen mode

Clojure:

(require '[clojure.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}
Enter fullscreen mode Exit fullscreen mode

Virtually identical semantics, different syntax. Clojure sets are also immutable and can be used as functions: (a 3) returns 3 (truthy), (a 7) returns nil (falsy).

List Comprehensions

Python:

[x**2 for x in range(10) if x % 2 == 0]
Enter fullscreen mode Exit fullscreen mode

Clojure:

(for [x (range 10) :when (even? x)]
  (* x x))
Enter fullscreen mode Exit fullscreen mode

Clojure's for is a list comprehension, not a loop. It supports :when for filtering, :let for local bindings, and multiple binding forms for nested iteration — all lazily evaluated.

Looping Techniques

Python:

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

for q, a in zip(questions, answers):
    print(q, a)
Enter fullscreen mode Exit fullscreen mode

Clojure:

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

(doseq [[q a] (map vector questions answers)]
  (println q a))
Enter fullscreen mode Exit fullscreen mode

Or more idiomatically, just use map to transform and print later:

(map-indexed (fn [i v] [i v]) ["tic" "tac" "toe"])
(map vector questions answers)
Enter fullscreen mode Exit fullscreen mode

6. Modules and Namespaces

Python:

import math
from os.path import join
import numpy as np
Enter fullscreen mode Exit fullscreen mode

Clojure:

(require '[clojure.string :as str])
(require '[clojure.java.io :as io])
(import '[java.util Date])
Enter fullscreen mode Exit fullscreen mode

Python organizes code into modules (files) and packages (directories with __init__.py). Clojure organizes code into namespaces — each file declares a namespace with ns, and dependencies are explicit:

(ns myapp.core
  (:require [clojure.string :as str]
            [cheshire.core :as json])
  (:import [java.time LocalDate]))
Enter fullscreen mode Exit fullscreen mode

There's no from X import * equivalent in Clojure, and that's by design. Explicit is better than implicit — a principle Pythonistas already appreciate.

The if __name__ == "__main__" Pattern

Python:

if __name__ == "__main__":
    main()
Enter fullscreen mode Exit fullscreen mode

Clojure doesn't need this pattern because the REPL workflow means you rarely "run a file." When you do need an entry point, you declare a -main function:

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

7. Input and Output

String Formatting

Python:

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

Clojure:

(str "Hello, " name "!")            ; simple concatenation
(format "Hello, %s!" name)          ; printf-style
(format "%.2f" 3.14159)             ; "3.14"
Enter fullscreen mode Exit fullscreen mode

Clojure uses Java's String.format under the hood. There's no f-string equivalent, but str for concatenation and format for templates cover the same ground.

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")
Enter fullscreen mode Exit fullscreen mode

Clojure:

(slurp "data.txt")                         ; read entire file
(spit "output.txt" "Hello\n")             ; write entire file

;; For line-by-line processing:
(with-open [rdr (clojure.java.io/reader "data.txt")]
  (doseq [line (line-seq rdr)]
    (println line)))
Enter fullscreen mode Exit fullscreen mode

slurp and spit — arguably the best-named I/O functions in any language. For structured work, with-open mirrors Python's with statement.

JSON

Python:

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

Clojure (with cheshire or clojure.data.json):

(require '[cheshire.core :as json])
(json/parse-string "{\"name\": \"Ada\"}" true)  ;=> {:name "Ada"}
(json/generate-string {:name "Ada"})             ;=> "{\"name\":\"Ada\"}"
Enter fullscreen mode Exit fullscreen mode

The true argument keywordizes the keys — JSON maps become idiomatic Clojure maps instantly.


8. Errors and Exceptions

Try / Catch

Python:

try:
    x = int(input("Number: "))
except ValueError as e:
    print(f"Invalid: {e}")
finally:
    print("Done")
Enter fullscreen mode Exit fullscreen mode

Clojure:

(try
  (Integer/parseInt (read-line))
  (catch NumberFormatException e
    (println "Invalid:" (.getMessage e)))
  (finally
    (println "Done")))
Enter fullscreen mode Exit fullscreen mode

Since Clojure runs on the JVM, it catches Java exceptions. The structure is almost identical to Python's try/except/finally.

Raising Exceptions

Python:

raise ValueError("something went wrong")
Enter fullscreen mode Exit fullscreen mode

Clojure:

(throw (ex-info "something went wrong" {:type :validation}))
Enter fullscreen mode Exit fullscreen mode

ex-info is idiomatic Clojure — it creates an exception that carries a data map. Instead of defining custom exception classes, you attach structured data to a generic exception. This is the "data over classes" philosophy in action:

(try
  (throw (ex-info "bad input" {:field :email :value "not-an-email"}))
  (catch clojure.lang.ExceptionInfo e
    (println (ex-data e))))
;=> {:field :email, :value "not-an-email"}
Enter fullscreen mode Exit fullscreen mode

Custom Exceptions

Python:

class InsufficientFunds(Exception):
    def __init__(self, balance, amount):
        self.balance = balance
        self.amount = amount
Enter fullscreen mode Exit fullscreen mode

Clojure rarely defines custom exception classes. Instead:

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

One generic mechanism, infinite data shapes. No class hierarchy to maintain.


9. Classes and Objects

This is where Python and Clojure diverge most dramatically.

Python's Object-Oriented Approach

class Dog:
    kind = "canine"             # class variable

    def __init__(self, name):
        self.name = name        # instance variable

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

rex = Dog("Rex")
rex.speak()  # "Rex says woof!"
Enter fullscreen mode Exit fullscreen mode

Clojure's Data-Oriented Approach

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

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

(speak rex)  ;=> "Rex says woof!"
Enter fullscreen mode Exit fullscreen mode

No class. No self. No constructor. Just a map and a function. The dog is its data.

Inheritance vs. Composition

Python:

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

class Dog(Animal):
    def speak(self):
        return "Woof!"

class Cat(Animal):
    def speak(self):
        return "Meow!"
Enter fullscreen mode Exit fullscreen mode

Clojure uses multimethods or protocols for polymorphism:

;; Multimethod approach — dispatch on data
(defmulti speak :type)
(defmethod speak :dog [animal] "Woof!")
(defmethod speak :cat [animal] "Meow!")

(speak {:type :dog :name "Rex"})   ;=> "Woof!"
(speak {:type :cat :name "Whiskers"}) ;=> "Meow!"
Enter fullscreen mode Exit fullscreen mode
;; Protocol approach — like interfaces
(defprotocol Speakable
  (speak [this]))

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

(speak (->Dog "Rex"))  ;=> "Rex says Woof!"
Enter fullscreen mode Exit fullscreen mode

Multimethods dispatch on any function of the arguments (not just type), making them more flexible than traditional OOP dispatch.

Iterators and Generators

Python:

class Reverse:
    def __init__(self, data):
        self.data = data
        self.index = len(data)

    def __iter__(self):
        return self

    def __next__(self):
        if self.index == 0:
            raise StopIteration
        self.index -= 1
        return self.data[self.index]
Enter fullscreen mode Exit fullscreen mode
def reverse(data):
    for index in range(len(data) - 1, -1, -1):
        yield data[index]
Enter fullscreen mode Exit fullscreen mode

Clojure:

(reverse "spam")  ;=> (\m \a \p \s)
(rseq [1 2 3 4])  ;=> (4 3 2 1)
Enter fullscreen mode Exit fullscreen mode

Clojure's sequences are lazy iterators. There's no iterator protocol to implement — every collection already participates in the sequence abstraction. Need a custom lazy sequence? Use lazy-seq:

(defn countdown [n]
  (when (pos? n)
    (lazy-seq (cons n (countdown (dec n))))))

(countdown 5)  ;=> (5 4 3 2 1)
Enter fullscreen mode Exit fullscreen mode

Generator Expressions

Python:

sum(x*x for x in range(10))
Enter fullscreen mode Exit fullscreen mode

Clojure:

(reduce + (map #(* % %) (range 10)))
;; or with the threading macro:
(->> (range 10) (map #(* % %)) (reduce +))
Enter fullscreen mode Exit fullscreen mode

The threading macro ->> reads like a Unix pipeline and is one of Clojure's most beloved features.


10. The Standard Library

Python's "Batteries Included"

Python ships with modules for OS interaction, file I/O, regex, math, dates, HTTP, email, testing, logging, threading, and much more — all in the standard library.

Clojure's Approach

Clojure's standard library is smaller but remarkably powerful for data manipulation. For everything else, you tap into:

  1. The entire Java ecosystem — need HTTP? Use java.net.http. Need dates? Use java.time. Need crypto? Use javax.crypto.
  2. Clojure community libraries — managed via deps.edn or Leiningen.
Python Module Clojure Equivalent
os, shutil clojure.java.io, Java NIO
re re-find, re-matches, re-seq (built-in)
math clojure.math (1.11+), java.lang.Math
datetime java.time (via tick library for ergonomics)
json clojure.data.json or cheshire
unittest clojure.test (built-in)
logging tools.logging + logback
threading core.async, future, pmap, agents
collections Built into the core (persistent data structures)
argparse tools.cli
sqlite3 next.jdbc + any JDBC driver

Concurrency — Where Clojure Truly Shines

Python's threading story involves the GIL and careful locking. Clojure was designed for concurrency from day one:

  • Atoms — uncoordinated, synchronous state updates
  • Refs — coordinated, transactional state (Software Transactional Memory)
  • Agents — asynchronous state updates
  • core.async — CSP-style channels (like Go's goroutines)
(def counter (atom 0))
(swap! counter inc)      ;=> 1  (thread-safe, no locks)
@counter                 ;=> 1

;; Process 1000 items in parallel:
(pmap expensive-fn (range 1000))
Enter fullscreen mode Exit fullscreen mode

11. Advanced Standard Library

Output Formatting

Python: pprint, textwrap, locale
Clojure: clojure.pprint/pprint (built-in), Java's Locale

(require '[clojure.pprint :refer [pprint]])
(pprint {:a 1 :b {:c [1 2 3] :d "hello"}})
Enter fullscreen mode Exit fullscreen mode

Templating

Python: string.Template
Clojure: clojure.core/format, or libraries like Selmer for HTML templating.

Multi-threading

Python:

import threading
t = threading.Thread(target=worker)
t.start()
Enter fullscreen mode Exit fullscreen mode

Clojure:

(future (worker))  ; runs in a thread pool, returns a deref-able future
@(future (+ 1 2))  ;=> 3
Enter fullscreen mode Exit fullscreen mode

Logging

Python: logging.warning("Watch out!")
Clojure:

(require '[clojure.tools.logging :as log])
(log/warn "Watch out!")
Enter fullscreen mode Exit fullscreen mode

Decimal Arithmetic

Python: decimal.Decimal("0.1") + decimal.Decimal("0.2")
Clojure:

(+ 0.1M 0.2M)  ;=> 0.3M  (BigDecimal literal with M suffix)
Enter fullscreen mode Exit fullscreen mode

Clojure has literal syntax for BigDecimals (M suffix) and BigIntegers (N suffix) — no imports needed.


12. Virtual Environments and Packages

Python uses venv and pip:

python -m venv myenv
source myenv/bin/activate
pip install requests
pip freeze > requirements.txt
Enter fullscreen mode Exit fullscreen mode

Clojure uses deps.edn (official) or project.clj (Leiningen):

;; deps.edn
{:deps {org.clojure/clojure {:mvn/version "1.12.0"}
        cheshire/cheshire {:mvn/version "5.13.0"}}}
Enter fullscreen mode Exit fullscreen mode
clj -M -m myapp.core  # run with dependencies resolved automatically
Enter fullscreen mode Exit fullscreen mode

There's no virtual environment concept because dependencies are resolved per-project from the deps.edn file. Maven coordinates ensure reproducibility. No activate/deactivate dance.


13. Floating-Point Arithmetic

Both languages sit on top of IEEE 754 doubles:

>>> 0.1 + 0.2 == 0.3
False
Enter fullscreen mode Exit fullscreen mode
(= (+ 0.1 0.2) 0.3)  ;=> false
Enter fullscreen mode Exit fullscreen mode

Same hardware, same surprise. But Clojure offers escape hatches as first-class citizens:

;; Ratios — exact arithmetic, no precision loss
(+ 1/10 2/10)      ;=> 3/10
(= (+ 1/10 2/10) 3/10)  ;=> true

;; BigDecimals
(+ 0.1M 0.2M)      ;=> 0.3M
Enter fullscreen mode Exit fullscreen mode

Clojure's ratio type means you can do exact fractional arithmetic without importing anything. This alone has saved countless financial applications from rounding bugs.


14. Interactive Editing

Python supports readline-based history and tab completion in its interactive interpreter.

Clojure developers typically use nREPL connected to their editor (Emacs + CIDER, VS Code + Calva, IntelliJ + Cursive). The experience goes far beyond line editing — you can evaluate any expression in your source file, inspect results inline, and navigate documentation without leaving your editor.


The Big Picture

Dimension Python Clojure
Paradigm Multi-paradigm (imperative + OOP + functional) Functional-first (with pragmatic escape hatches)
Mutability Mutable by default Immutable by default
Type System Dynamic, gradual typing via hints Dynamic, with optional specs
Concurrency GIL, async/await, multiprocessing STM, atoms, agents, core.async
Syntax Indentation-based, keyword-rich S-expressions, minimal syntax
OOP Classes, inheritance, dunder methods Protocols, multimethods, plain maps
Runtime CPython, PyPy JVM, JavaScript (ClojureScript)
Package Manager pip + venv deps.edn / Leiningen + Maven
REPL Culture Strong Even stronger
Ideal For Scripts, ML/AI, web, automation Data processing, concurrency, web, DSLs

Closing Thoughts

Python is a phenomenal language. Its readability, ecosystem, and community have earned it the top spot for good reason. But if you've ever felt the friction of debugging shared mutable state, wrestling with class hierarchies, or wishing your data transformations could be simpler — Clojure might be the language that makes you see programming differently.

You don't have to abandon Python. Many developers use both: Python for its unmatched ML/AI ecosystem and scripting ergonomics, Clojure for systems where correctness, concurrency, and data transformation matter most.

The best way to start is to fire up a REPL. Try Clojure's official getting started guide, or experiment in the browser at repl.it. The parentheses will feel strange for an hour. Then they'll feel like home.


This article was written as a companion to the official Python tutorial. Every section maps to a chapter in that tutorial, translated through the lens of Clojure's philosophy: simple, data-driven, and functional.

Top comments (0)