Learn what `def` actually creates, when it belongs in a namespace, and why it is not the same as a mutable local variable.
def is one of the first forms Java developers overinterpret in Clojure.
It does not mean “ordinary variable assignment” in the Java sense.
What it actually does is create or update a Var in the current namespace and, when you provide an initial value, set that var’s root binding.
1(def answer 42)
2answer
3;; => 42
4
5#'answer
6;; => #'current-ns/answer
That distinction matters because a var is a named slot in the namespace, while the value stored there can be any immutable value or function.
For day-to-day reading, this table is the safest model.
| Form | What it creates | Typical use |
|---|---|---|
def |
A namespace var | Top-level named values, REPL definitions, special cases |
defn |
A var whose value is a function | Ordinary named functions in a namespace |
let |
Local bindings | Temporary names inside an expression |
fn |
A function value | Anonymous functions or function literals |
Java analogy:
let is closer to a local variable namedef is closer to introducing a namespace-level namedefn is the normal way to publish a function from that namespaceThat is better than thinking “Clojure variables are all globals.”
def Is The Right ToolUse def at the top level of a namespace when you want a stable name for something readers should be able to refer to directly.
Common examples:
1(def default-timeout-ms 5000)
2
3(def currency-scale 2)
4
5(def supported-statuses
6 #{:draft :submitted :paid :cancelled})
These are ordinary top-level definitions. They are easy to discover, easy to inspect at the REPL, and easy to reuse from other functions.
def is also normal during interactive work:
1(def sample-order
2 {:order/id 1001
3 :status :draft})
That REPL-friendly workflow is one reason vars exist in the first place. You can define and redefine named values while exploring code.
def Is Notdef is not the form you use for routine step-by-step computation inside functions.
This is the wrong instinct:
1(defn total-with-tax [amount]
2 (def tax-rate 0.13M) ;; bad style
3 (* amount (+ 1 tax-rate)))
The function works, but tax-rate is now a namespace var, not a local name. That means the function leaked a global definition as a side effect.
The right tool is let:
1(defn total-with-tax [amount]
2 (let [tax-rate 0.13M]
3 (* amount (+ 1 tax-rate))))
That keeps the temporary binding local to the function body.
This is the subtle part that trips up many Java engineers.
The value 42 is immutable. The map stored in a var is immutable. The function stored in a var is immutable as a value.
But the var itself can later be redefined:
1(def tax-rate 0.13M)
2(def tax-rate 0.15M)
That is not in-place mutation of a local variable. It is rebinding the root of the namespace var to a new value.
In production code, you usually want such redefinition to be intentional and rare. At the REPL, it is normal and useful.
Use def for:
Do not use def for:
If you are thinking “I need to change this as the function runs,” you probably want either:
let, or