Scoping and Shadowing¶
Overview¶
Nix uses lexical scoping: a name resolves to the binding established by the nearest enclosing construct that introduced it. Functions, let, and rec attribute sets add names to the lexical environment; normal attribute sets do not. The with expression is a special case — it adds soft bindings that can be overridden by let, function parameters, and other non-with bindings.
Understanding which constructs introduce bindings — and which merely read values — prevents subtle name-resolution bugs in large expressions and nixpkgs-style call patterns.
Details¶
What introduces lexical bindings¶
| Construct | Names in scope |
|---|---|
Function { x, y ? x }: … |
Pattern names (and @-pattern names) for the entire function expression, including default values |
let … in … |
All let-bound names in the let block and in the body |
rec { … } |
Attribute names refer to each other inside the set |
{ … } (non-rec) |
Attribute names are not in lexical scope outside attribute values |
with set; body |
Set attributes available in body only (soft bindings) |
inherit / inherit (src) … |
Copies names into a let or attrset definition from outer scope or src |
set.${name} |
Not a binding — dynamic attribute selection only |
Normal attrsets vs rec¶
In a non-recursive attribute set, x = y; refers to y from the surrounding lexical scope (if any), not to another attribute in the same set:
Without an outer y, { x = y; } is a free-variable error. With rec, attributes are added to the lexical scope of the set body:
See lists and attrsets for construction syntax.
Function pattern scope¶
All bindings from a function pattern are visible throughout the function expression — not only in the body after ::
Here y's default [ x ] may refer to x because both are in the same pattern scope. The same applies to @-patterns: args is bound to the argument as passed (defaults applied only when the body reads the corresponding name).
See functions for pattern forms and default-value semantics.
inherit¶
Inside a let block or attribute set, inherit foo; is shorthand for foo = foo;, copying from the outer lexical scope. inherit (set) a b; copies set.a and set.b. This only works because the surrounding let (or the fact that you are defining bindings) puts the right-hand names in scope.
with: soft bindings and shadowing¶
A with set; body expression makes the attributes of set available while evaluating body. These are soft bindings:
- Non-
withbindings win. Alet, lambda parameter, or other binding introduced outsidewithshadows attributes brought in bywith, even if thewithappears textually closer to the use site. - Nested
withshadows outerwith. An innerwithhides same-named attributes from an outerwith. - Rewriting rule. The manual gives an equivalence: each
withcan be thought of as aletthat binds the set's attributes, and those converted lets nest outside explicitletbindings — so explicit bindings end up inner and take priority.
The let-in and with page covers syntax; the examples below focus on resolution order.
Dynamic attribute selection is not scoping¶
Selecting set.${var} or defining ${var} = value in a set uses the value of var to pick or name an attribute. That does not introduce var's referent as a new binding in the surrounding expression:
Do not confuse this with with or inherit.
NixOS module scope (sketch)¶
Module function arguments (config, pkgs, lib, …) are lexical bindings from the function pattern — not with imports. inherit (config) services copies attributes from config into a nested attrset you are defining; it does not add services as a global name in the module body.
{ config, lib, ... }: {
# `services` is NOT in scope here — use config.services or inherit explicitly:
environment.systemPackages = lib.optionals config.services.openssh.enable [
config.pkgs.openssh
];
}
Common pitfalls¶
| Mistake | Effect | Fix |
|---|---|---|
{ x = y; } without outer y or rec |
Free variable error | Use rec or bind y in outer let |
Large with pkgs; in modules |
Accidental shadowing, hard-to-grep names | Prefer pkgs.hello or explicit inherit (pkgs) … |
if config.foo.enable then { … } cycles |
Infinite recursion during merge | mkIf on definitions |
Confusing rec with // merge |
Expect deep merge; get shallow replace | overlay shallow-merge rules |
Boundaries (what this page is not)¶
- Not
withtutorial syntax — patterns and style are let-in and with. - Not function pattern reference —
@, defaults, and...are functions. - Not module merge semantics —
mkMerge/ priorities are mkIf / mkMerge / mkOrder.
Examples¶
let wins over with¶
The with binding never overrides the explicit let.
Nested let and with — manual equivalence¶
The manual states that:
is equivalent to:
Reading inside-out: converted with layers (1, then 2) sit outside the explicit lets (3, then 4). The innermost binding wins, so a is 4.
Inner with shadows outer with¶
Both layers are with; the inner set's a hides the outer one.
rec vs outer scope¶
Inside rec, the attribute x is in scope for sibling values. The outer let x is shadowed by the recursive attribute.
Non-rec set uses outer x¶
Without rec, x in the attribute value resolves to the enclosing let.
Default values see sibling pattern bindings¶
y's default expression runs in the function's pattern scope, so x is available.
inherit in a module attrset¶
{ config, pkgs, ... }: {
services.nginx = {
enable = true;
virtualHosts."localhost" = {
root = pkgs.writeTextDir "index.html" "<h1>hi</h1>";
};
};
# Equivalent to copying names from config — does not import all of config:
# inherit (config.services.nginx) enable;
}
Try name resolution in nix repl with small let / with snippets before debugging large modules.
See also¶
- let-in and with — syntax and common
withpatterns - Functions — parameter patterns and defaults
- Lists and attrsets —
rec,inherit, attrset construction - Anti-patterns — when to avoid
with