Agents convert-python-clojure
Convert Python code to idiomatic Clojure. Use when migrating Python projects to Clojure, translating Python patterns to idiomatic Clojure, or refactoring Python codebases to leverage functional programming. Extends meta-convert-dev with Python-to-Clojure specific patterns.
git clone https://github.com/aRustyDev/agents
T=$(mktemp -d) && git clone --depth=1 https://github.com/aRustyDev/agents "$T" && mkdir -p ~/.claude/skills && cp -r "$T/content/skills/convert-python-clojure" ~/.claude/skills/arustydev-agents-convert-python-clojure && rm -rf "$T"
content/skills/convert-python-clojure/SKILL.mdConvert Python to Clojure
Convert Python code to idiomatic Clojure. This skill extends
meta-convert-dev with Python-to-Clojure specific type mappings, idiom translations, and tooling for transforming imperative, object-oriented Python code into functional, immutable Clojure.
This Skill Extends
- Foundational conversion patterns (APTV workflow, testing strategies)meta-convert-dev
For general concepts like the Analyze → Plan → Transform → Validate workflow, testing strategies, and common pitfalls, see the meta-skill first.
This Skill Adds
- Type mappings: Python types → Clojure types (dynamic → dynamic with immutability)
- Idiom translations: Python patterns → idiomatic Clojure (OOP → functional)
- Error handling: Exceptions → explicit error values
- Async patterns: asyncio → core.async
- Data structures: Mutable collections → immutable persistent collections
- REPL workflow: Script-based → REPL-driven development
This Skill Does NOT Cover
- General conversion methodology - see
meta-convert-dev - Python language fundamentals - see
lang-python-dev - Clojure language fundamentals - see
lang-clojure-dev - Reverse conversion (Clojure → Python) - see
convert-clojure-python - ClojureScript - see
convert-python-clojurescript
Quick Reference
| Python | Clojure | Notes |
|---|---|---|
| / / | Clojure has arbitrary precision |
| | 64-bit floating point |
| / | Direct mapping |
| | Java strings (immutable) |
| | Mutable byte array |
| | Vector (indexed, immutable) |
| or | Vector or list |
| | Hash map (immutable) |
| | Hash set (immutable) |
| | Absence of value |
| / | Data structures |
| | Function definition |
| or | Anonymous function |
| | Side-effecting iteration |
| | Lazy sequence comprehension |
| | Exception handling |
| core.async channels | Different concurrency model |
When Converting Code
- Analyze source thoroughly before writing target
- Map types first - Python and Clojure are both dynamic but Clojure emphasizes immutability
- Embrace immutability - replace in-place mutations with functional transformations
- Use REPL-driven development - Clojure is designed for interactive development
- Adopt functional idioms - avoid classes, prefer pure functions and data transformations
- Handle edge cases - None→nil, exceptions, mutable state
- Test equivalence - same inputs → same outputs
Type System Mapping
Primitive Types
| Python | Clojure | Notes |
|---|---|---|
| / | Automatic promotion to BigInteger |
| | 64-bit IEEE 754 |
| / | Lowercase boolean literals |
| | Java.lang.String (immutable) |
| | Mutable Java byte array |
| | Same as bytes in Clojure |
| | Represents absence |
(Ellipsis) | - | No equivalent |
Note on Integers: Both Python and Clojure support arbitrary precision integers. Clojure automatically promotes from
long to BigInteger on overflow.
Collection Types
| Python | Clojure | Notes |
|---|---|---|
| | Vector - indexed, immutable, O(log32 n) |
| | Vector (immutable by default) |
| | List for sequential access |
| | Hash map - immutable, O(log32 n) |
| | Hash set - immutable |
| | Sets are immutable by default |
| | Immutable queue |
| or | Maintains insertion order |
| Map + with default | Use pattern |
| function | Built-in frequency counter |
| | Lazy sequence |
Composite Types
| Python | Clojure | Notes |
|---|---|---|
| | Named fields, map-like access |
(data) | or plain map | Prefer maps for simple data |
(behavior) | Protocols + | Polymorphism via protocols |
| | Named, typed fields |
| Plain map | Maps with keyword keys |
| Keyword enum | |
| Tagged map or multimethod | |
| or value | represents absence |
| or | First-class functions |
| Clojure protocol | Polymorphism |
Idiom Translation
Pattern 1: List Comprehensions → Sequence Operations
Python:
# List comprehension squared_evens = [x * x for x in numbers if x % 2 == 0] # Nested comprehension pairs = [(x, y) for x in range(3) for y in range(2)] # Generator expression total = sum(x * x for x in numbers if x % 2 == 0)
Clojure:
;; for - lazy sequence comprehension (def squared-evens (for [x numbers :when (even? x)] (* x x))) ;; Nested for - cartesian product (def pairs (for [x (range 3) y (range 2)] [x y])) ;; Direct transformation with threading (def total (->> numbers (filter even?) (map #(* % %)) (reduce +)))
Why this translation:
- Python list comprehensions are eager; Clojure
is lazy (more efficient)for - Clojure's threading macros (
) make pipelines clearer->>
is idiomatic for aggregationreduce
Pattern 2: Dictionary Operations → Map Operations
Python:
# Get with default value = config.get("timeout", 30) # Setdefault pattern cache.setdefault(key, expensive_compute()) # Dictionary comprehension squared = {k: v * v for k, v in items.items()} # Merging dictionaries merged = {**dict1, **dict2}
Clojure:
;; Get with default (def value (get config :timeout 30)) ;; Lazy computation with update (def cache (update cache key #(or % (expensive-compute)))) ;; Or with caching (using memoize) (defn get-cached [cache key compute-fn] (if-let [v (get cache key)] v (let [v (compute-fn)] (assoc cache key v)))) ;; Map transformation (def squared (into {} (map (fn [[k v]] [k (* v v)]) items))) ;; Merging maps (def merged (merge dict1 dict2))
Why this translation:
- Clojure maps are immutable; use
,assoc
,update
for "changes"merge - Keywords (
) are idiomatic for map keys:timeout
+into
is the comprehension pattern for mapsmap
Pattern 3: Classes → Records and Maps
Python:
from dataclasses import dataclass @dataclass class User: id: int name: str email: str def full_info(self): return f"{self.name} ({self.email})" # Usage user = User(id=1, name="Alice", email="alice@example.com") print(user.name) print(user.full_info())
Clojure:
;; defrecord for structured data (defrecord User [id name email]) ;; Constructor and access (def user (->User 1 "Alice" "alice@example.com")) (println (:name user)) ; Map-like access ;; Functions operate on data (defn full-info [user] (str (:name user) " (" (:email user) ")")) (println (full-info user)) ;; Alternative: plain map (often preferred for simple data) (def user {:id 1 :name "Alice" :email "alice@example.com"}) (println (:name user)) (println (full-info user))
Why this translation:
- Clojure separates data from behavior (functions operate on data structures)
provides type identity and performance benefitsdefrecord- Plain maps are often simpler and more flexible than records
- Functions are defined separately, not as methods
Pattern 4: Iteration → Sequence Operations
Python:
# Imperative loop with mutation result = [] for item in items: if item > 0: result.append(item * 2) # Enumerate for i, item in enumerate(items): print(f"{i}: {item}") # Zip for name, age in zip(names, ages): print(f"{name} is {age}")
Clojure:
;; Functional transformation (immutable) (def result (->> items (filter pos?) (map #(* % 2)))) ;; map-indexed (like enumerate) (doseq [[i item] (map-indexed vector items)] (println (str i ": " item))) ;; map for pairing (like zip) (doseq [[name age] (map vector names ages)] (println (str name " is " age)))
Why this translation:
- Clojure favors pure transformations over imperative loops
,filter
,map
are core sequence operationsreduce
is for side effects (like printing),doseq
is for lazy sequencesfor
andmap-indexed
replace Python's enumerate and zipmap vector
Pattern 5: None Handling → nil Handling
Python:
# None checks if user is not None: name = user.name else: name = "Anonymous" # Or with walrus if (user := get_user(id)) is not None: process(user) # Default value name = user.name if user else "Anonymous"
Clojure:
;; nil checks with when-let (when-let [user (get-user id)] (process user)) ;; Or with if-let (def name (if-let [user user] (:name user) "Anonymous")) ;; Or with threading (some-> stops on nil) (def name (some-> user :name (str " is here"))) ;; Default value with or (def name (or (:name user) "Anonymous"))
Why this translation:
- Clojure uses
instead ofnilNone
andwhen-let
bind and test in one stepif-let
andsome->
short-circuit onsome->>
(like optional chaining)nil
returns first truthy value (like Python'sor
)or
Pattern 6: Exceptions → Explicit Error Handling
Python:
# Raising exceptions def divide(a, b): if b == 0: raise ValueError("Division by zero") return a / b # Catching exceptions try: result = divide(10, 0) except ValueError as e: print(f"Error: {e}") result = None # Finally block try: file = open("data.txt") data = file.read() finally: file.close()
Clojure:
;; Throwing exceptions (when appropriate) (defn divide [a b] (if (zero? b) (throw (ex-info "Division by zero" {:a a :b b})) (/ a b))) ;; Catching exceptions (def result (try (divide 10 0) (catch Exception e (println "Error:" (.getMessage e)) nil))) ;; with-open for resource cleanup (like Python's with) (with-open [rdr (clojure.java.io/reader "data.txt")] (def data (slurp rdr))) ;; Functional error handling (preferred for non-exceptional cases) (defn safe-divide [a b] (if (zero? b) {:error "Division by zero"} {:ok (/ a b)})) (let [result (safe-divide 10 0)] (if (:error result) (println "Error:" (:error result)) (println "Result:" (:ok result))))
Why this translation:
- Clojure uses exceptions for truly exceptional cases
- For expected errors, return maps with
/:ok
keys (or use a library like:error
for Either)cats
ensures resource cleanup (like Python's context managers)with-open
creates exceptions with data mapsex-info
Pattern 7: Decorators → Macros or Higher-Order Functions
Python:
# Function decorator def logged(func): def wrapper(*args, **kwargs): print(f"Calling {func.__name__}") result = func(*args, **kwargs) print(f"Finished {func.__name__}") return result return wrapper @logged def process(data): return len(data) # Property decorator class Circle: def __init__(self, radius): self._radius = radius @property def area(self): return 3.14159 * self._radius ** 2
Clojure:
;; Higher-order function (decorator-like) (defn logged [f] (fn [& args] (println "Calling" (str f)) (let [result (apply f args)] (println "Finished" (str f)) result))) (def process (logged (fn [data] (count data)))) ;; Or as a macro (compile-time transformation) (defmacro defn-logged [name args & body] `(defn ~name ~args (println "Calling" ~(str name)) (let [result# (do ~@body)] (println "Finished" ~(str name)) result#))) (defn-logged process [data] (count data)) ;; "Property" via function (Clojure has no properties) (defrecord Circle [radius]) (defn area [circle] (* 3.14159 (:radius circle) (:radius circle))) (def c (->Circle 5)) (println (area c))
Why this translation:
- Python decorators → Clojure higher-order functions or macros
- Macros run at compile time, can transform syntax
- Clojure has no property syntax; use plain functions
for code transformation,defmacro
for runtime wrappingdefn
Pattern 8: Object-Oriented → Functional
Python:
# Class with state and methods class Counter: def __init__(self): self.count = 0 def increment(self): self.count += 1 def get(self): return self.count counter = Counter() counter.increment() counter.increment() print(counter.get()) # 2
Clojure:
;; Immutable data + pure functions (defn increment [counter] (update counter :count inc)) (defn get-count [counter] (:count counter)) ;; Usage with threading (-> {:count 0} (increment) (increment) (get-count) (println)) ; 2 ;; For mutable state, use atoms (def counter (atom {:count 0})) (defn increment! [counter] (swap! counter update :count inc)) (defn get-count! [counter] (:count @counter)) (increment! counter) (increment! counter) (println (get-count! counter)) ; 2
Why this translation:
- Clojure prefers immutable data + pure functions over mutable objects
threading macro passes result through function chain->- For necessary state, use atoms (
for updates,swap!
for reads)@ - Separate data (maps) from behavior (functions)
Pattern 9: String Formatting → str and format
Python:
# f-strings name = "Alice" age = 30 message = f"Hello {name}, you are {age} years old" # format method message = "Hello {}, you are {} years old".format(name, age) # % formatting message = "Hello %s, you are %d years old" % (name, age)
Clojure:
;; str concatenation (def name "Alice") (def age 30) (def message (str "Hello " name ", you are " age " years old")) ;; format (uses Java String.format) (def message (format "Hello %s, you are %d years old" name age)) ;; Or using clojure.pprint for complex formatting (require '[clojure.pprint :refer [cl-format]]) (def message (cl-format nil "Hello ~A, you are ~D years old" name age))
Why this translation:
concatenates arguments (simple, idiomatic)str
uses Java'sformat
(printf-style)String.format
(Common Lisp format) for advanced formattingcl-format
Pattern 10: Async/Await → core.async
Python:
import asyncio async def fetch_user(user_id): await asyncio.sleep(0.1) # Simulate I/O return {"id": user_id, "name": f"User {user_id}"} async def main(): user = await fetch_user(123) print(user) asyncio.run(main())
Clojure:
(require '[clojure.core.async :as async :refer [go <! >! chan]]) ;; core.async uses channels for communication (defn fetch-user [user-id] (go (<! (async/timeout 100)) ; Simulate I/O {:id user-id :name (str "User " user-id)})) (defn main [] (let [result-chan (fetch-user 123) user (<!! result-chan)] ; <!! blocks, <! for within go block (println user))) (main) ;; Or with go blocks and channels (go (let [user (<! (fetch-user 123))] (println user)))
Why this translation:
- Python's async/await → Clojure's
blocks and channelsgo
takes from channel (inside<!
),go
blocks (outside<!!
)go
puts onto channel,>!
blocks>!!- Different paradigm: CSP (channels) vs promises/futures
Error Handling
Python Exceptions → Clojure Approaches
| Python | Clojure | Notes |
|---|---|---|
| | Direct exception |
| | Exception with data |
| | Catching exceptions |
| | Cleanup block |
Return for errors | Return or | Explicit error values |
Exception Handling Translation
Python:
def load_config(path): try: with open(path) as f: data = json.load(f) return data except FileNotFoundError: print(f"Config file not found: {path}") return None except json.JSONDecodeError as e: print(f"Invalid JSON: {e}") return None finally: print("Cleanup")
Clojure:
(require '[clojure.data.json :as json]) (defn load-config [path] (try (-> path slurp json/read-str) (catch java.io.FileNotFoundException e (println "Config file not found:" path) nil) (catch Exception e (println "Invalid JSON:" (.getMessage e)) nil) (finally (println "Cleanup"))))
Why this translation:
- Similar try/catch/finally structure
- Clojure uses Java exception classes
reads entire file (like Python'sslurp
)read()
parses JSON stringjson/read-str
Functional Error Handling (Preferred)
Python (using optional types):
from typing import Optional def safe_divide(a: int, b: int) -> Optional[float]: if b == 0: return None return a / b result = safe_divide(10, 0) if result is not None: print(f"Result: {result}") else: print("Division by zero")
Clojure (using maps or nil):
;; Returning nil for errors (defn safe-divide [a b] (when-not (zero? b) (/ a b))) (if-let [result (safe-divide 10 0)] (println "Result:" result) (println "Division by zero")) ;; Or using explicit error maps (defn safe-divide [a b] (if (zero? b) {:error "Division by zero"} {:ok (/ a b)})) (let [result (safe-divide 10 0)] (if (:error result) (println "Error:" (:error result)) (println "Result:" (:ok result))))
Why this translation:
- Nil represents absence/failure (like Python's None)
- Maps with
/:ok
keys make errors explicit:error - Functional error handling avoids exception overhead for common cases
Concurrency Patterns
Python Threading/Asyncio → Clojure Concurrency
| Python | Clojure | Notes |
|---|---|---|
| or futures | Java threads |
| or | Async execution |
| or | Concurrent ops |
| | Async channel |
| , , or Java locks | Coordinated state |
| , | Async results |
Asyncio → core.async
Python:
import asyncio async def fetch_data(url): await asyncio.sleep(0.1) # Simulate I/O return f"Data from {url}" async def main(): # Concurrent execution results = await asyncio.gather( fetch_data("url1"), fetch_data("url2"), fetch_data("url3") ) for result in results: print(result) asyncio.run(main())
Clojure:
(require '[clojure.core.async :as async :refer [go <! >! chan]]) (defn fetch-data [url] (go (<! (async/timeout 100)) ; Simulate I/O (str "Data from " url))) ;; Concurrent execution with channels (defn main [] (let [urls ["url1" "url2" "url3"] channels (map fetch-data urls)] ;; Collect results (doseq [ch channels] (println (<!! ch))))) (main) ;; Or using alts! for first-to-complete (go (let [urls ["url1" "url2" "url3"] channels (map fetch-data urls) [result ch] (async/alts! channels)] (println "First result:" result)))
Why this translation:
- Python's async/await uses event loop; Clojure uses CSP channels
blocks are lightweight (like goroutines)go- Channels (
) pass values between go blockschan
is likealts!
in Go (first-to-complete)select
Threading → Atoms and Futures
Python:
from concurrent.futures import ThreadPoolExecutor def process_item(item): return item * 2 with ThreadPoolExecutor(max_workers=4) as executor: results = list(executor.map(process_item, range(10))) print(results)
Clojure:
;; pmap - parallel map (uses futures) (def results (pmap #(* % 2) (range 10))) (println (doall results)) ; Force realization ;; Or explicit futures (def results (doall (map #(future (* % 2)) (range 10)))) ;; Dereference futures to get values (def values (map deref results)) (println values) ;; Or use thread pool explicitly (import '[java.util.concurrent Executors]) (def executor (Executors/newFixedThreadPool 4)) (def tasks (map #(.submit executor ^Callable (fn [] (* % 2))) (range 10))) (def results (map #(.get %) tasks)) (.shutdown executor)
Why this translation:
is parallel map (automatic thread pool)pmap
creates async task,future
orderef
waits for result@- Can use Java executors for fine-grained control
State Management
Python:
import threading counter = 0 lock = threading.Lock() def increment(): global counter with lock: counter += 1 threads = [threading.Thread(target=increment) for _ in range(100)] for t in threads: t.start() for t in threads: t.join() print(counter) # 100
Clojure:
;; Atoms for uncoordinated state (def counter (atom 0)) (defn increment! [] (swap! counter inc)) ;; Parallel updates (thread-safe) (doall (pmap (fn [_] (increment!)) (range 100))) (println @counter) ; 100 ;; Or refs for coordinated transactions (def account-a (ref 100)) (def account-b (ref 200)) (defn transfer [from to amount] (dosync (alter from - amount) (alter to + amount))) (transfer account-a account-b 50) (println @account-a @account-b) ; 50 250
Why this translation:
- Atoms for independent state (
for atomic updates)swap! - Refs for coordinated state (
for transactions)dosync - No locks needed - Clojure's concurrency primitives are thread-safe
Common Pitfalls
1. Mutable State → Immutable Data
Problem:
;; Python: in-place mutation # items.append(value) # items[0] = new_value ;; Clojure: trying to mutate (def items [1 2 3]) (conj items 4) ; Returns new vector, doesn't modify items! (println items) ; Still [1 2 3]
Solution:
;; Rebind with new value (def items (conj items 4)) ; Now items is [1 2 3 4] ;; Or use atoms for mutable state (def items (atom [1 2 3])) (swap! items conj 4) (println @items) ; [1 2 3 4] ;; Or work with local bindings (let [items [1 2 3] items (conj items 4) items (conj items 5)] (println items)) ; [1 2 3 4 5]
Why this matters: Clojure's persistent data structures are immutable by default.
2. Truthiness Differences
Problem:
;; Python: empty collections are falsy # if items: # True for [1, 2], False for [] ;; Clojure: empty collections are truthy! (if [] "truthy" "falsy") ; => "truthy"
Solution:
;; Explicitly check for emptiness (if (seq items) "has items" "empty") ;; Or use empty? (if-not (empty? items) "has items" "empty")
Why this matters: Only
nil and false are falsy in Clojure. Empty collections are truthy.
3. Sequence Realization
Problem:
;; Lazy sequences aren't realized until needed (def nums (map #(do (println "Computing" %) (* % 2)) [1 2 3])) ;; Nothing printed yet! (count nums) ; Now it prints "Computing 1" "Computing 2" "Computing 3"
Solution:
;; Force realization with doall (def nums (doall (map #(do (println "Computing" %) (* % 2)) [1 2 3]))) ;; Immediately prints ;; Or use doseq for side effects (doseq [x [1 2 3]] (println "Computing" x))
Why this matters: Clojure sequences are lazy by default. Side effects in lazy sequences may not execute when expected.
4. Keyword vs String Keys
Problem:
;; Python: strings as keys # user = {"name": "Alice", "age": 30} ;; Clojure: mixing keywords and strings (def user {"name" "Alice" :age 30}) ; Inconsistent! (:name user) ; nil (looking for keyword, but key is string)
Solution:
;; Use keywords consistently (def user {:name "Alice" :age 30}) (:name user) ; "Alice" ;; Or strings consistently (less idiomatic) (def user {"name" "Alice" "age" 30}) (get user "name") ; "Alice"
Why this matters: Keywords (
:name) are idiomatic for map keys in Clojure. They're faster and work as functions.
5. Namespace Collisions
Problem:
;; Python: methods are namespaced by class # user.get("name") # config.get("timeout") ;; Clojure: same function name across namespaces (require '[clojure.set :as set]) (set/union #{1 2} #{2 3}) ; Must qualify or alias
Solution:
;; Always use namespace aliases (require '[clojure.string :as str] '[clojure.set :as set]) (str/upper-case "hello") (set/union #{1 2} #{2 3}) ;; Or refer specific functions (require '[clojure.string :refer [upper-case lower-case]]) (upper-case "hello")
Why this matters: Clojure namespaces prevent collisions but require explicit imports.
6. Integer Division
Problem:
;; Python 3: / always returns float # 5 / 2 # 2.5 ;; Clojure: / returns ratio for integers (/ 5 2) ; 5/2 (ratio), not 2.5
Solution:
;; Convert to double for floating-point division (/ 5.0 2) ; 2.5 ;; Or use quot for integer division (quot 5 2) ; 2 ;; Force ratio to double (double (/ 5 2)) ; 2.5
Why this matters: Clojure preserves exact ratios. Use
double or floating-point literals for decimals.
7. Variadic Functions
Problem:
;; Python: *args, **kwargs # def func(*args, **kwargs): # print(args, kwargs) ;; Clojure: rest args only (no keyword args) (defn func [& args] (println args)) (func 1 2 3) ; (1 2 3)
Solution:
;; Use destructuring for keyword-style args (defn func [& {:keys [name age] :or {age 0}}] (println name age)) (func :name "Alice" :age 30) ; "Alice 30" (func :name "Bob") ; "Bob 0" (default age) ;; Or use maps explicitly (defn func [opts] (println (:name opts) (:age opts 0))) (func {:name "Alice" :age 30})
Why this matters: Clojure doesn't have keyword arguments. Use map destructuring or explicit maps.
8. Global Mutable State
Problem:
;; Python: global keyword # counter = 0 # def increment(): # global counter # counter += 1 ;; Clojure: def creates immutable binding (def counter 0) (defn increment [] (def counter (inc counter))) ; Bad! Creates new binding (increment) (println counter) ; Still 0!
Solution:
;; Use atoms for mutable state (def counter (atom 0)) (defn increment! [] (swap! counter inc)) (increment!) (println @counter) ; 1 ;; Or pass state explicitly (functional style) (defn increment [counter] (inc counter)) (-> 0 increment increment increment println) ; 3
Why this matters:
def creates new vars; use atoms for mutable state or pass state explicitly.
Tooling
Translation Tools
| Tool | Purpose | Notes |
|---|---|---|
| Manual translation | Full control | Recommended for production |
| REPL experimentation | Interactive development | Core Clojure workflow |
| Python-Clojure interop | Call Python from Clojure (libpython-clj) |
Development Environment
| Python | Clojure | Purpose |
|---|---|---|
| or | REPL |
/ | Leiningen or tools.deps | Package management |
| | Testing framework |
| | Runtime validation |
| | Code formatting |
| , | Linting |
Common Library Equivalents
| Python Package | Clojure Library | Purpose |
|---|---|---|
| | HTTP client |
| | Async HTTP |
/ | + | Web framework |
| | Data validation |
/ | | CLI parsing |
| | Logging |
| | Date/time |
| | File I/O |
| | JSON parsing |
| | Regex |
| | Database |
| | Data frames |
| | Numerical computing |
Examples
Example 1: Simple - HTTP GET Request
Before (Python):
import requests def fetch_user(user_id): response = requests.get(f"https://api.example.com/users/{user_id}") response.raise_for_status() return response.json() try: user = fetch_user(123) print(f"User: {user['name']}") except requests.HTTPError as e: print(f"HTTP error: {e}")
After (Clojure):
(require '[clj-http.client :as http] '[clojure.data.json :as json]) (defn fetch-user [user-id] (-> (str "https://api.example.com/users/" user-id) http/get :body (json/read-str :key-fn keyword))) ;; Usage (try (let [user (fetch-user 123)] (println "User:" (:name user))) (catch Exception e (println "HTTP error:" (.getMessage e))))
Key changes:
→requests.getclj-http.client/get- Dictionary access
→ keyword accessuser['name'](:name user)
withjson/read-str
converts JSON keys to keywords:key-fn keyword- Similar try/catch structure
Example 2: Medium - Configuration Parser
Before (Python):
from pathlib import Path import json from dataclasses import dataclass @dataclass class Config: host: str port: int timeout: int = 30 def validate(self): if not (1 <= self.port <= 65535): raise ValueError(f"Invalid port: {self.port}") def load_config(path): if not path.exists(): raise FileNotFoundError(f"Config not found: {path}") with path.open() as f: data = json.load(f) config = Config(**data) config.validate() return config config = load_config(Path("config.json")) print(f"Server: {config.host}:{config.port}")
After (Clojure):
(require '[clojure.data.json :as json] '[clojure.spec.alpha :as s]) ;; Define spec for validation (s/def ::host string?) (s/def ::port (s/and int? #(<= 1 % 65535))) (s/def ::timeout (s/and int? pos?)) (s/def ::config (s/keys :req-un [::host ::port] :opt-un [::timeout])) (defn load-config [path] (when-not (.exists (clojure.java.io/file path)) (throw (ex-info "Config not found" {:path path}))) (let [config (-> path slurp (json/read-str :key-fn keyword) (merge {:timeout 30}))] ; Default value (when-not (s/valid? ::config config) (throw (ex-info "Invalid config" (s/explain-data ::config config)))) config)) ;; Usage (let [config (load-config "config.json")] (println (format "Server: %s:%d" (:host config) (:port config))))
Key changes:
→ plain map with@dataclass
validationclojure.spec- Default values via
merge
for validation (runtime checks)clojure.spec
for exceptions with dataex-info
Example 3: Complex - Data Processing Pipeline
Before (Python):
from collections import defaultdict from dataclasses import dataclass from typing import List @dataclass class Transaction: user_id: int amount: float category: str def process_transactions(transactions: List[Transaction]): # Group by user by_user = defaultdict(list) for txn in transactions: by_user[txn.user_id].append(txn) # Calculate totals per category for each user results = {} for user_id, txns in by_user.items(): category_totals = defaultdict(float) for txn in txns: category_totals[txn.category] += txn.amount # Only users with total > 100 total = sum(category_totals.values()) if total > 100: results[user_id] = { "total": total, "by_category": dict(category_totals), "count": len(txns) } return results transactions = [ Transaction(1, 50.0, "food"), Transaction(1, 75.0, "transport"), Transaction(2, 200.0, "food"), Transaction(1, 25.0, "food"), ] results = process_transactions(transactions) for user_id, stats in results.items(): print(f"User {user_id}: {stats}")
After (Clojure):
(defn process-transactions [transactions] (->> transactions ;; Group by user (group-by :user-id) ;; Transform each user's transactions (map (fn [[user-id txns]] (let [;; Calculate category totals by-category (->> txns (group-by :category) (map (fn [[cat items]] [cat (reduce + (map :amount items))])) (into {})) total (reduce + (vals by-category)) count (count txns)] [user-id {:total total :by-category by-category :count count}]))) ;; Filter users with total > 100 (filter (fn [[_ stats]] (> (:total stats) 100))) ;; Convert to map (into {}))) ;; Usage (def transactions [{:user-id 1 :amount 50.0 :category "food"} {:user-id 1 :amount 75.0 :category "transport"} {:user-id 2 :amount 200.0 :category "food"} {:user-id 1 :amount 25.0 :category "food"}]) (def results (process-transactions transactions)) (doseq [[user-id stats] results] (println (format "User %d: %s" user-id stats)))
Key changes:
→ plain maps@dataclass- Imperative loops → functional pipeline with
threading->>
→defaultdict
functiongroup-by
loops →for
,map
,filterreduce- Immutable transformations throughout
- More declarative, less mutable state
See Also
For more examples and patterns, see:
- Foundational patterns with cross-language examplesmeta-convert-dev
- Python development patternslang-python-dev
- Clojure development patternslang-clojure-dev
- Async/channels patterns across languagespatterns-concurrency-dev
- JSON/EDN serialization patternspatterns-serialization-dev