Nix Language Primer

Nix is a package manager much like apt, yum or homebrew. Nix is also a language by which the nix package manager specifies the packages offered.

While nix (the language, not the package manager) is a complete programming language, it is not general-purpose. Its primary focus is declarative. It is used to create sets of parameters to be fed to external compilation tools such as configure and gcc.

This is a primer for the nix language. It is meant to enable you to read nix expressions. It does not attempt, for example, to teach you about the nix build system, nor how to best compose package definitions.

Since any nix “program” is fundamentally a single nix expression, the primary goal is to enable you to see where the sub-parts of an expression begin and end.

For those coming from other languages, nix can be confusing due to its use of semicolons and braces. In this respect, nix is different than many languages because it does not employ semicolons to separate statements. Instead, semicolons play a role more similar to commas in other languages, separating elements within part of an expression. Likewise, braces do not denote blocks, but rather sets and set patterns.

You will see more on this in the following sections.


Playing with expressions in the nix command-line interpreter (Read-Evaluate-Print-Loop) is encouraged.

In nix 1:

nix-env -ibA nixpkgs.nix-repl

Nix 2 includes the repl by default. You can access with:

nix repl


Whitespace generally does not matter in nix.


# trailing comment

/* multiline comment */


Nix is a pure functional language. There are no statements in nix, only a top-level, single expression which in turn is composed of other expressions. Every expression can be evaluated down to either a function or a value.

Expressions are composed from values, functions (called lambdas) and operations (operators with operands), as well as a handful of special keyword expressions.

You can use parentheses around any expression for clarity or to force precedence.

Boolean Values







"hello, world!"

"a multiline

''a string containing the " symbol''

Interpolation (“Anti-quotation”)

"a string with a ${nix_expression}"



Files are a basic type (they are not strings, so no quotes). They must always contain a /.

Relative pathnames should always start with ./ to ensure the presence of at least one /.

Relative paths are relative to the file in which they appear.


URLs evaluate to strings, but have their own literal format. They do not require quotes.


[ "one" "two" "three" ]

Lists are a container type for multiple values.

Lists can be empty:

[ ]

List elements can be of diverse types, including other lists or sets, even within the same list.

Lists are not typically used as frequently as sets are.


Sets are the workhorse of nix.

Sets are a container type for multiple key/value pairs. They are analogous to hashes in other languages.

Nix refers to the keys as “attributes”.

{ key1 = "value1"; key2 = "value2"; }

The final semicolon is required.

Accessing values in a set:

{ key = "value"; }.key

Sets can be empty:

{ }

Note that, in nix, braces are only used for sets and set patterns (see below). There are no brace-delimited blocks as there are in other languages.

For example, while the following may look like a function definition in another language, a function name followed by braces is actually a function invocation with an empty set as an argument, not a definition:

do_something { }

Function Definition and Patterns

Functions are anonymous, meaning they don’t have names in their definitions. To give a name to a function, you bind it to a key or a variable just as you would with any other value.

Functions start with an argument pattern:

# pattern: function body
  arg:     do_something_with arg  # do_something_with is a made-up function

“Argument pattern” is a fancy name for “give me a (single) variable name for the argument”.

There is only ever one argument to a function, however that one argument may be a set, in which case you may use a set pattern in the declaration:

# { set pattern                  }: function body
  { arg1, arg2 ? "default_value" }: do_something_with (arg1 && arg2)

Set patterns consist of the key names (with default values, if desired) separated by commas, which become variables in the function scope.

Note that set patterns are the only construct in nix which uses commas, and they don’t need a final comma before the closing brace (unlike nix’s semicolon-based expressions).

Function Invocation

Functions are invoked by passing an argument after a space. Parentheses around the argument(s) are generally not required.

Note that you don’t need to name the function, just pass an argument after its declaration (parentheses for precedence):

# (function definition       ) <string argument>
  (arg: do_something_with arg) "value"

Functions specified with a set pattern must receive a set with exactly the required keys and nothing more (minus any defaults, if desired):

# (function definition           ) <set argument>
  ({ arg }: do_something_with arg) { arg = "value"; }

Functions with “Multiple” Arguments

Functions can only take one argument, but they can return a function which then takes the next argument:

arg1: arg2: do_something_with (arg1 && arg2)

Which syntactically breaks down to:

# (function 1    (function 2                            ))
  (arg1:         (arg2: do_something_with (arg1 && arg2)))

Calling the outer function with one argument returns the inner function, curried with arg1 (i.e. a function with a closure containing arg1). That function may be later called just by providing arg2.


Nix is lexically scoped like most languages. Variables always resolve the same way based on the local scope first, then up through parent expression scopes, up to the global scope as necessary. The matching name in the closest scope to the executing code is the value to which the variable is resolved.

In the multi-argument function above, arg1 is available to the inner function because arg1 is in the outer function’s scope. Since it is the parent expression, its scope is available to the child unless the child masks that name with its own variable of the same name.


Variables are names which can hold the result of any expression, usually values or functions.

Functions stored in variables can be called by invoking the variable name with an argument.

The keys of sets are similar to variables, and can be extracted into variables, but they are distinct concepts.

Recursive Sets

A set’s values can refer to variables, but they can’t refer to other keys in the set. Those are keys, not variables.

For example, this doesn’t work:

{ a = 1; b = a; }

Recursive sets make the set’s own keys available as variables within the scope of the set, including its subexpressions. Recursive sets employ the rec keyword, followed by the usual set notation:

rec { a = 1; b = a; }

Note that rec is a keyword, not a variable containing a function. You cannot have a variable named “rec”.


There are no assignment statements in nix, but you can create a new scope that has “bindings” (another name for assignment) with the let expression:

  a = 1;
  b = 2;
  do_something_with a

Remember that whitespace doesn’t matter so you don’t need the above indentation.

The final semicolon in the let portion is required.

The in portion of the expression has access to the variables defined in the let portion.

The variables are not available to anything outside the let expression, only to the in portion.

The variables will mask variables of the same name of an outer scope.

The expression as a whole evaluates to the value of the in portion of the expression.

Note that because there are no assignment statements in nix, you cannot modify the global scope. You can only create bindings in subscopes.

Lazy Evaluation

Bindings are only evaluated if they are referenced by the resulting expression. For example, b does not throw a divide by 0 error in the following expression because it is not referenced by the in expression:

  a = 1;
  b = 1 / 0;
  do_something_with a

Bindings may refer to other bindings within the same let, but again, since they are only evaluated when referenced by the in expression, order doesn’t matter. For example, the following is fine, even though a refers to b before b is defined:

  a = b;
  b = 1;
  do_something_with a

Within container types (sets and lists), values are only evaluated insofar as they reach another container type and no further. So only functions, operations and basic values are evaluated upon reference.

The unevaluated elements of container types become further evaluated when their key (or list element) is referenced directly.


As a convenience, you can extract the keys of a set into variables via the with expression:

with { a = 1; b = 2; }; do_something_with a

The variables a and b will be in scope for the do_something_with a expression, similar to the in portion of a let expression.

Further with expressions inside the second portion of an outer with can mask variables created by the outer with.

Variables created via with will not, however, mask variables created by an outer let expression, a recursive set (see below), nor the global namespace. This can lead to unexpected results, depending on the environment in which the with expression is evaluated.

As an example, try this expression:

with { builtins = "hello"; }; builtins

There is no way to extract only a subset of a set’s keys via a simple with.


I won’t go over each of the builtins, but all of the functions available out-of-the-box with nix are stored in a global set called builtins.

You can examine the names of the available functions with:

builtins.attrNames builtins

A few builtins are also available directly in the global namespace, such as toString:

toString ./filename.txt


Derivations are the set of information needed to classify and build a package. Derivations are a their own type, layered over a basic set.

Derivations are created with the derivation function. It takes, at a minimum, the set of name, builder and system:

derivation { name = "myname"; builder = "mybuilder"; system = "mysystem"; }

The result of this function call is a special set (type: derivation) which looks like the following:

  all         = [ «derivation /nix/store/z3hhlxbckx4g3n9sw91nnvlkjvyw754p-myname.drv» ] ;
  builder     = "mybuilder"                                                             ;
  drvAttrs    = { builder = "mybuilder"; name = "myname"; system = "mysystem"; }        ;
  drvPath     = "/nix/store/z3hhlxbckx4g3n9sw91nnvlkjvyw754p-myname.drv"                ;
  name        = "myname"                                                                ;
  out         = «derivation /nix/store/z3hhlxbckx4g3n9sw91nnvlkjvyw754p-myname.drv»     ;
  outPath     = "/nix/store/40s0qmrfb45vlh6610rk29ym318dswdr-myname"                    ;
  outputName  = "out"                                                                   ;
  system      = "mysystem"                                                              ;
  type        = "derivation"                                                            ;

While it is its own type, it can still be treated as a normal set.


The inherit keyword in a set definition creates a key with the same value as the variable of the same name:

  a = 1;
  { inherit a; }

This is the same as:

  a = 1;
  { a = a; }

inherit can take multiple arguments:

  a = 1;
  b = 2;
  { inherit a b; }


A package is typically a function which produces a derivation:

{ system ? builtins.currentSystem }:
  derivation {
    name    = "myname"    ;
    builder = "mybuilder" ;
    inherit system        ;


A channel is a function which produces a set of derivations:

{ system ? builtin.currentSystem }:
    builder = "mybuilder";
      package1 = derivation { name = "package1"; inherit builder system; };
      package2 = derivation { name = "package2"; inherit builder system; };

Typically a channel is constructed with package imports which are then invoked with an argument, since packages are functions that produce derivations, rather than direct derivation calls as shown here.


The import keyword expression loads the expression in the given file:

import ./expression.nix

The expression in the file is returned as if it had been source code in place of the import expression, with one important difference. The imported expression cannot see any outer scopes except for the global scope.

Because of this, most file-based expressions are functions, so anything they need from the outer scope can be explicitly passed to them.

Importing a directory path causes the file default.nix in that directory to be loaded.

For example, the following expression loads the file default.nix from the same directory in which the expression’s own file is located:

import ./.

Nix Path

In addition to files, you can use nix path references with import:

import <nixpkgs>

The angle brackets are literal and instruct nix to consult the NIX_PATH environment variable for resolution.

NIX_PATH contains a colon-delimited string of key=path pairs. The keys are available as expansions to nix using the angle brackets, and the values are the paths to which those keys expand.

Per usual, if the path is a directory, then it references default.nix in that directory.


That is a brief survey of the most important facets of the nix language.

There are many other important features and details, such as how builders work, the mkDerivation helper, if then else expressions and more.

Some good sources of information include:

The nix-repl referenced at the beginning of the cheatsheet is an invaluable tool for learning the ins and outs of the language.

Finally, there is a dated but illuminating description of an early form of the nix language grammar.