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.
When & Why to Use This Skill
This Claude skill automates the technical translation of Python code into idiomatic Clojure, streamlining the migration from imperative, object-oriented systems to functional, immutable architectures. It provides comprehensive mappings for data types, asynchronous patterns (asyncio to core.async), and error handling, ensuring that converted code adheres to Clojure's best practices and REPL-driven development workflows.
Use Cases
- Migrating legacy Python microservices to Clojure to benefit from functional programming paradigms and improved concurrency management.
- Translating complex Python data processing scripts into idiomatic Clojure using threading macros and lazy sequences for better performance.
- Refactoring object-oriented Python classes into Clojure records and protocols to achieve a cleaner separation of data and behavior.
- Converting Python's asynchronous logic and asyncio patterns into Clojure's core.async CSP model for robust concurrent operations.
- Assisting developers in cross-language learning by providing idiomatic Clojure equivalents for common Python design patterns.
| name | convert-python-clojure |
|---|---|
| description | 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. |
Convert 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
meta-convert-dev- Foundational conversion patterns (APTV workflow, testing strategies)
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 |
|---|---|---|
int |
int / long / BigInt |
Clojure has arbitrary precision |
float |
double |
64-bit floating point |
bool |
true / false |
Direct mapping |
str |
String |
Java strings (immutable) |
bytes |
byte-array |
Mutable byte array |
list[T] |
[...] |
Vector (indexed, immutable) |
tuple |
[...] or (list ...) |
Vector or list |
dict[K, V] |
{:key val} |
Hash map (immutable) |
set[T] |
#{...} |
Hash set (immutable) |
None |
nil |
Absence of value |
class |
defrecord / deftype |
Data structures |
def func(): |
(defn func [] ...) |
Function definition |
lambda x: x*2 |
#(* % 2) or (fn [x] (* x 2)) |
Anonymous function |
for x in xs: |
(doseq [x xs] ...) |
Side-effecting iteration |
[x for x in xs] |
(for [x xs] ...) |
Lazy sequence comprehension |
try/except |
(try ... (catch ...)) |
Exception handling |
async def |
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 |
|---|---|---|
int |
int / long |
Automatic promotion to BigInteger |
float |
double |
64-bit IEEE 754 |
bool |
true / false |
Lowercase boolean literals |
str |
String |
Java.lang.String (immutable) |
bytes |
byte-array |
Mutable Java byte array |
bytearray |
byte-array |
Same as bytes in Clojure |
None |
nil |
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 |
|---|---|---|
list[T] |
[...] |
Vector - indexed, immutable, O(log32 n) |
tuple[T, U] |
[...] |
Vector (immutable by default) |
tuple[T, ...] |
(list ...) |
List for sequential access |
dict[K, V] |
{:key val} |
Hash map - immutable, O(log32 n) |
set[T] |
#{...} |
Hash set - immutable |
frozenset[T] |
#{...} |
Sets are immutable by default |
collections.deque |
clojure.lang.PersistentQueue |
Immutable queue |
collections.OrderedDict |
(array-map ...) or linked-hash-map |
Maintains insertion order |
collections.defaultdict |
Map + get-in with default |
Use (get m k default) pattern |
collections.Counter |
frequencies function |
Built-in frequency counter |
range(n) |
(range n) |
Lazy sequence |
Composite Types
| Python | Clojure | Notes |
|---|---|---|
@dataclass |
defrecord |
Named fields, map-like access |
class (data) |
defrecord or plain map |
Prefer maps for simple data |
class (behavior) |
Protocols + defrecord |
Polymorphism via protocols |
typing.NamedTuple |
defrecord |
Named, typed fields |
typing.TypedDict |
Plain map {:key val} |
Maps with keyword keys |
enum.Enum |
Keyword enum | #{:state/pending :state/done} |
typing.Union[T, U] |
Tagged map or multimethod | {:type :int :value 42} |
typing.Optional[T] |
nil or value |
nil represents absence |
typing.Callable |
fn or IFn |
First-class functions |
typing.Protocol |
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
foris lazy (more efficient) - Clojure's threading macros (
->>) make pipelines clearer reduceis idiomatic for aggregation
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,mergefor "changes" - Keywords (
:timeout) are idiomatic for map keys into+mapis the comprehension pattern for maps
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)
defrecordprovides type identity and performance benefits- 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,reduceare core sequence operationsdoseqis for side effects (like printing),foris for lazy sequencesmap-indexedandmap vectorreplace Python's enumerate and zip
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
nilinstead ofNone when-letandif-letbind and test in one stepsome->andsome->>short-circuit onnil(like optional chaining)orreturns first truthy value (like Python'sor)
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/:errorkeys (or use a library likecatsfor Either) with-openensures resource cleanup (like Python's context managers)ex-infocreates exceptions with data maps
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
defmacrofor code transformation,defnfor runtime wrapping
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 (
swap!for updates,@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:
strconcatenates arguments (simple, idiomatic)formatuses Java'sString.format(printf-style)cl-format(Common Lisp format) for advanced formatting
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
goblocks and channels <!takes from channel (insidego),<!!blocks (outsidego)>!puts onto channel,>!!blocks- Different paradigm: CSP (channels) vs promises/futures
Error Handling
Python Exceptions → Clojure Approaches
| Python | Clojure | Notes |
|---|---|---|
raise Exception("msg") |
(throw (Exception. "msg")) |
Direct exception |
raise ValueError(...) |
(throw (ex-info "msg" {:data ...})) |
Exception with data |
try: ... except E: ... |
(try ... (catch E e ...)) |
Catching exceptions |
try: ... finally: ... |
(try ... (finally ...)) |
Cleanup block |
Return None for errors |
Return nil or {:error ...} |
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
slurpreads entire file (like Python'sread())json/read-strparses JSON string
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/:errorkeys make errors explicit - Functional error handling avoids exception overhead for common cases
Concurrency Patterns
Python Threading/Asyncio → Clojure Concurrency
| Python | Clojure | Notes |
|---|---|---|
threading.Thread |
(Thread. ...) or futures |
Java threads |
asyncio.run(coro) |
(go ...) or (future ...) |
Async execution |
asyncio.gather(*tasks) |
(async/alts! ...) or pmap |
Concurrent ops |
asyncio.Queue |
(chan) |
Async channel |
threading.Lock |
(atom), (ref), or Java locks |
Coordinated state |
concurrent.futures |
future, promise |
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
goblocks are lightweight (like goroutines)- Channels (
chan) pass values between go blocks alts!is likeselectin Go (first-to-complete)
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:
pmapis parallel map (automatic thread pool)futurecreates async task,derefor@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 (
swap!for atomic updates) - Refs for coordinated state (
dosyncfor transactions) - 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 |
clj-python |
Python-Clojure interop | Call Python from Clojure (libpython-clj) |
Development Environment
| Python | Clojure | Purpose |
|---|---|---|
python |
clj or lein repl |
REPL |
pip / uv |
Leiningen or tools.deps | Package management |
pytest |
clojure.test |
Testing framework |
mypy |
clojure.spec |
Runtime validation |
black |
cljfmt |
Code formatting |
pylint |
eastwood, kibit |
Linting |
Common Library Equivalents
| Python Package | Clojure Library | Purpose |
|---|---|---|
requests |
clj-http |
HTTP client |
aiohttp |
http-kit |
Async HTTP |
flask / fastapi |
ring + compojure |
Web framework |
pydantic |
clojure.spec |
Data validation |
click / argparse |
tools.cli |
CLI parsing |
logging |
tools.logging |
Logging |
datetime |
clj-time |
Date/time |
pathlib |
clojure.java.io |
File I/O |
json |
clojure.data.json |
JSON parsing |
re |
clojure.string |
Regex |
sqlite3 |
clojure.java.jdbc |
Database |
pandas |
tech.ml.dataset |
Data frames |
numpy |
core.matrix |
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.get→clj-http.client/get- Dictionary access
user['name']→ keyword access(:name user) json/read-strwith:key-fn keywordconverts JSON keys to keywords- 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:
@dataclass→ plain map withclojure.specvalidation- Default values via
merge clojure.specfor validation (runtime checks)ex-infofor exceptions with data
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:
@dataclass→ plain maps- Imperative loops → functional pipeline with
->>threading defaultdict→group-byfunctionforloops →map,filter,reduce- Immutable transformations throughout
- More declarative, less mutable state
See Also
For more examples and patterns, see:
meta-convert-dev- Foundational patterns with cross-language exampleslang-python-dev- Python development patternslang-clojure-dev- Clojure development patternspatterns-concurrency-dev- Async/channels patterns across languagespatterns-serialization-dev- JSON/EDN serialization patterns