Agent Skills: Convert Python to 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.

UncategorizedID: arustydev/ai/convert-python-clojure

Repository

aRustyDevLicense: AGPL-3.0
72

Install this agent skill to your local

pnpm dlx add-skill https://github.com/aRustyDev/agents/tree/HEAD/content/skills/convert-python-clojure

Skill Files

Browse the full folder contents for convert-python-clojure.

Download Skill

Loading file tree…

content/skills/convert-python-clojure/SKILL.md

Skill Metadata

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

  1. Analyze source thoroughly before writing target
  2. Map types first - Python and Clojure are both dynamic but Clojure emphasizes immutability
  3. Embrace immutability - replace in-place mutations with functional transformations
  4. Use REPL-driven development - Clojure is designed for interactive development
  5. Adopt functional idioms - avoid classes, prefer pure functions and data transformations
  6. Handle edge cases - None→nil, exceptions, mutable state
  7. 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 for is lazy (more efficient)
  • Clojure's threading macros (->>) make pipelines clearer
  • reduce is 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, merge for "changes"
  • Keywords (:timeout) are idiomatic for map keys
  • into + map is 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)
  • defrecord provides 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, reduce are core sequence operations
  • doseq is for side effects (like printing), for is for lazy sequences
  • map-indexed and map vector replace 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 nil instead of None
  • when-let and if-let bind and test in one step
  • some-> and some->> short-circuit on nil (like optional chaining)
  • or returns first truthy value (like Python's 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 / :error keys (or use a library like cats for Either)
  • with-open ensures resource cleanup (like Python's context managers)
  • ex-info creates 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
  • defmacro for code transformation, defn for 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:

  • str concatenates arguments (simple, idiomatic)
  • format uses Java's String.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 go blocks and channels
  • <! 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 | |--------|---------|-------| | 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
  • slurp reads entire file (like Python's read())
  • json/read-str parses 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/:error keys 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
  • go blocks are lightweight (like goroutines)
  • Channels (chan) pass values between go blocks
  • alts! is like select in 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:

  • pmap is parallel map (automatic thread pool)
  • future creates async task, deref or @ 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 (dosync for 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.getclj-http.client/get
  • Dictionary access user['name'] → keyword access (:name user)
  • json/read-str with :key-fn keyword converts 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 with clojure.spec validation
  • Default values via merge
  • clojure.spec for validation (runtime checks)
  • ex-info for 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
  • defaultdictgroup-by function
  • for loops → 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 examples
  • lang-python-dev - Python development patterns
  • lang-clojure-dev - Clojure development patterns
  • patterns-concurrency-dev - Async/channels patterns across languages
  • patterns-serialization-dev - JSON/EDN serialization patterns