let-in and with¶
Overview¶
Nix provides two scoping constructs beyond function parameters and attribute sets: let-in for local bindings, and with for bringing an attribute set's fields into lexical scope. Both support inherit and inherit (set) like attrset definitions. Prefer let when you need named locals — it is explicit about which names exist. Reserve with for short scopes where the set is obvious; mixed let/with shadowing is easy to misread (see below and scoping and shadowing).
Details¶
let-in¶
A let-expression binds one or more names, then evaluates a body:
This evaluates to "foobar". Bindings in the same let block share scope: later bindings may refer to earlier ones, and all bindings may refer to each other mutually (similar to rec within that block). The body may use any of the let-bound names.
inherit and inherit (src-set) a b … work in let the same way they do in attribute sets — they copy names from the surrounding scope or from another set into the let block.
Obsolete let { … body = …; } form¶
An older syntax wraps bindings in a recursive attribute set with a mandatory body attribute:
This evaluates to "baz". The form is recursive (like rec { … }) and should not be used in new code. Prefer let … in ….
with¶
A with-expression introduces the attributes of its first operand into the scope of its second:
If e1 evaluates to an attribute set, the names of that set become available when evaluating e2:
This evaluates to "foobar". A common pattern is with (import ./defs.nix); …, which makes attributes from that file available as if they were local let bindings.
Prefer let for clarity¶
let lists every introduced name next to its definition. with dumps an entire set into scope, so readers (and tools) cannot tell at a glance which free names came from the set versus outer bindings. Prefer let inherit (set) a b; or explicit let a = set.a; when only a few names are needed. Prefer set.attr selection when a single use is enough.
Shadowing rules¶
Bindings from with do not shadow bindings introduced by other means — let, function parameters, and other non-with bindings always win. For example, the manual states that:
is equivalent to:
Rewritten as nested lets, the with layers become the outer bindings (a = 1, then a = 2), and the explicit lets sit closer to the body (a = 3, then a = 4), so a resolves to 4. In short: non-with bindings take priority over with bindings because they end up inner in the equivalent nested-let form.
Nested with expressions do shadow outer with bindings — the inner set's attributes hide same-named attributes from an outer with.
Pitfall: a typo or renamed attribute in a with set can silently pick up an outer binding of the same name instead of failing, because the with binding never overrides that outer name. See scoping and shadowing for the full picture.
Examples¶
Local helpers with mutual reference (verified: "2.1"):
Inherit from an outer scope inside let (verified: { x = 123; y = 456; }):
Non-with binding wins over with (verified: "from-let"):
Inner with shadows outer with (verified: "inner"):
Selective inherit instead of a broad with (verified: { names = [ "a" "b" ]; }):
See also¶
- Lists and attrsets — attrset syntax and
inherit - Functions — parameter bindings and default values
- Scoping and shadowing — let/with interaction in depth
- Anti-patterns — when to avoid
with