Skip to content
Français
Account: not signed in, sync available My learning
Select theme
Français

The Nix language: types, functions, attrsets and expressions

By updated

Improve this page

The Nix language is a pure functional language designed to describe packages and configurations reproducibly. Everything in it is an expression: a number, a function, a configuration file, each element returns a value, nothing produces a side effect. This guide walks you through every fundamental concept by testing it immediately in the REPL or with nix eval, on Nix 2.35.2.

What you will learn

  • Handle the 7 primitive types of Nix (integers, floats, strings, booleans, null, paths, functions)
  • Structure data with attribute sets (attrsets) and lists
  • Define and call functions (simple, curried, with named arguments)
  • Use let...in, with, inherit and if-then-else
  • Transform data with the builtins (map, filter, attrNames…)
  • Write complete expressions ready for shell.nix or flake.nix

In what context?

The Nix language is the foundation of the whole ecosystem: every package is described by a Nix expression, every shell.nix, every flake.nix, every NixOS module is Nix code. Understanding the language lets you:

  • read and modify the shell.nix or flake.nix files of your projects,
  • write your own derivations to package software,
  • debug a development environment that does not behave as expected,
  • understand the examples of the nixpkgs and NixOS documentation,
  • customise a NixOS configuration module by module.

The REPL: your playground

The REPL (Read-Eval-Print Loop) is an interactive terminal to test Nix expressions. Start it with:

Terminal window
nix repl --expr '{}'

Type an expression, Nix evaluates it and prints the result. Type :q to quit.

You can also evaluate one-off expressions directly in the terminal with:

Terminal window
nix eval --expr '1 + 2'
Result
3

Every example of this guide can be tested with either method.

Primitive types

Nix has 7 basic types. You can check the type of any value with builtins.typeOf:

Integers and floats

Terminal window
nix eval --expr '1 + 2'
Result
3
Terminal window
nix eval --expr '7 / 2'
Result
3

Division between integers is integer division (no rounding, truncation). To get a decimal result:

Terminal window
nix eval --expr '7.0 / 2.0'
Result
3.5
Terminal window
nix eval --expr 'builtins.typeOf 42'
Result
"int"

Strings

Strings are delimited by double quotes:

Terminal window
nix eval --expr '"hello" + " world"'
Result
"hello world"

The + operator concatenates strings (and adds numbers, Nix tells them apart by the type of the operands).

Interpolation with ${}:

Terminal window
nix eval --expr 'let name = "Nix"; in "Hello ${name}"'
Result
"Hello Nix"

To interpolate a number, convert it with toString:

let port = 8080; in "Port: ${toString port}"
# "Port: 8080"

Multi-line strings, delimited by '' (two single quotes):

''
Première ligne
Deuxième ligne
''

The smallest common indentation is removed automatically.

Booleans

Booleans mostly drive if expressions and NixOS configuration attributes such as services.nginx.enable = true;. Two properties matter in practice: the && and || operators are lazy, they only evaluate the right-hand operand if the result is not already decided, and Nix accepts no implicit conversion. An integer, an empty string or null are not false: a condition must produce a real boolean.

Terminal window
nix eval --expr 'true && false'
Result
false
Terminal window
nix eval --expr 'true || false'
Result
true

Comparison operators: ==, !=, <, >, <=, >=.

Terminal window
nix eval --expr '3 > 2'
Result
true

Null and paths

null is not an empty string: it is the value that explicitly signals the absence of a value, widely used in NixOS options to say "keep the module's default value". The path is more surprising for anyone coming from another language: it is written without quotes and Nix resolves it at evaluation time. A path must contain at least one /, which explains the ./. form to designate the current directory.

null # type "null": absence of a value
/tmp # type "path": absolute path
./. # type "path": relative path

Summary of the types

The third column gives the exact string returned by builtins.typeOf, the one you will compare in your tests. Two naming traps: a function has the type "lambda", not "function", and an attrset has the type "set", not "attrset". This table covers the primitive types; on top of them come the two composite types described below, lists ("list") and attribute sets ("set").

TypeExamplesbuiltins.typeOf
Integer42, -1, 0"int"
Float3.14, 7.0"float"
String"hello", ''multi''"string"
Booleantrue, false"bool"
Nullnull"null"
Path/tmp, ./file.nix"path"
Functionx: x + 1"lambda"

Lists

Lists are ordered collections, delimited by square brackets and separated by spaces (no commas):

[ 1 2 3 ]
[ "hello" true 42 ] # mixed types allowed

List operations

Nix offers neither loops nor bracket indexing: writing list[1] is read as a function call and raises the error attempt to call something which is not a function. Positional access goes through builtins.elemAt list 1. The operations below form the basis of everything else. They are all non-destructive: builtins.tail modifies nothing, it returns a new list and the original stays intact, since values are immutable.

Terminal window
nix eval --expr 'builtins.length [ 1 2 3 ]'
Result
3
Terminal window
nix eval --expr 'builtins.head [ 10 20 30 ]'
Result
10
Terminal window
nix eval --expr 'builtins.tail [ 10 20 30 ]'
Result
[ 20 30 ]
Terminal window
nix eval --expr '[ 1 2 ] ++ [ 3 4 ]'
Result
[ 1 2 3 4 ]

The ++ operator concatenates two lists. builtins.elem tests membership:

Terminal window
nix eval --expr 'builtins.elem 3 [ 1 2 3 ]'
Result
true

Attribute sets (attrsets)

The attribute set is the fundamental data structure of Nix, a key-value dictionary. Each attribute ends with a ;:

{
name = "Alice";
age = 30;
active = true;
}

Access and manipulation

The dot goes down into an attrset, and it chains over several levels: config.services.nginx.enable is a simple nested access. Remember that accessing an absent attribute does not return null but raises an attribute missing evaluation error, which makes the two mechanisms seen right after essential, the operator ? and the keyword or.

Terminal window
nix eval --expr '{ a = 1; b = 2; }.a'
Result
1

Merging with //

The // operator merges two attrsets. The keys on the right-hand side overwrite those on the left:

Terminal window
nix eval --expr '{ a = 1; b = 2; } // { b = 99; c = 3; }'
Result
{ a = 1; b = 99; c = 3; }

Introspection

These two functions let you walk through an attrset whose keys you do not know in advance, a common situation when you explore nixpkgs or the configuration of a module. An important point for reproducibility: attrNames and attrValues always return the elements in the same order, sorted by attribute name, whatever the order in which they were written in the source file.

Terminal window
nix eval --expr 'builtins.attrNames { z = 1; a = 2; m = 3; }'
Result
[ "a" "m" "z" ]

The names are returned sorted alphabetically.

Terminal window
nix eval --expr 'builtins.attrValues { a = 10; b = 20; }'
Result
[ 10 20 ]

Existence test with ?

The ? operator answers the question "does this attribute exist", without raising an error and without evaluating its value. It is the test to use before any optional access. Do not confuse it with the ? of function arguments seen further down, which declares a default value: same character, two unrelated meanings.

Terminal window
nix eval --expr '{ a = 1; } ? a'
Result
true
Terminal window
nix eval --expr '{ a = 1; } ? b'
Result
false

Combined with or for a default value:

let config = { port = 8080; };
in config.host or "localhost"
# "localhost"

Recursive attrsets with rec

A normal attrset cannot reference its own attributes. With rec, it can:

rec {
name = "hello";
fullName = "${name}-world"; # uses 'name' from the same attrset
}
# { fullName = "hello-world"; name = "hello"; }

Functions

In Nix, functions are values like any other. The syntax is argument: body, the : separates the argument from the body.

Simple functions

Terminal window
nix eval --expr '(x: x * 2) 5'
Result
10

The function x: x * 2 takes an argument x and returns x * 2. It is called by passing 5 right after it.

Curried functions (several arguments)

Nix only supports one argument per function. For several arguments, functions are chained:

Terminal window
nix eval --expr '(a: b: a + b) 3 4'
Result
7

a: b: a + b is in fact a: (b: a + b), a function that returns a function.

Functions with named arguments (pattern matching)

This is the most common form in Nix files. The argument is a destructured attrset:

Terminal window
nix eval --expr '({ name, age }: "${name} a ${toString age} ans") { name = "Alice"; age = 30; }'
Result
"Alice a 30 ans"

Default values with ?

A default value makes an argument optional at call time. It is evaluated lazily, only if the caller provided nothing, and can therefore contain an expensive expression without penalising the other calls. This is exactly the mechanism that lets a shell.nix work with a plain nix-shell, without being passed any argument.

{ name, port ? 8080 }:
"Server ${name} on port ${toString port}"

If port is not provided, the value 8080 is used.

Extra arguments with ...

{ name, port ? 8080, ... }:
"Server ${name}"

The ... allows the caller to pass extra attributes without an error.

Local variables: let...in

The let...in block creates immutable local variables:

Terminal window
nix eval --expr 'let x = 5; y = 3; in x + y'
Result
8

The variables can reference each other:

let
base = 10;
double = base * 2;
triple = base * 3;
in
double + triple
# 50

with, bringing a scope in

The with keyword brings all the attributes of an attrset into the current scope:

Terminal window
nix eval --expr 'let s = { a = 1; b = 2; }; in with s; a + b'
Result
3

Typical use, avoiding repeating pkgs.:

# Without with
packages = [ pkgs.git pkgs.vim pkgs.ripgrep ];
# With with
packages = with pkgs; [ git vim ripgrep ];

inherit, assignment shorthand

inherit copies variables from the enclosing scope into an attrset:

Terminal window
nix eval --expr 'let x = 1; y = 2; in { inherit x y; }'
Result
{ x = 1; y = 2; }

inherit x y; is strictly equivalent to x = x; y = y;.

You can also inherit from another attrset:

let config = { port = 8080; host = "localhost"; };
in { inherit (config) port host; }
# { port = 8080; host = "localhost"; }

Conditionals: if-then-else

In Nix, if is an expression that always returns a value:

Terminal window
nix eval --expr 'if 3 > 2 then "oui" else "non"'
Result
"oui"

The else is mandatory, every expression must return a value.

Typical use, conditional packages:

let
debugMode = true;
debugTools = if debugMode then [ pkgs.htop pkgs.ncdu ] else [];
in
basePackages ++ debugTools

import, loading a Nix file

import loads and evaluates a .nix file:

# If config.nix contains { port = 8080; host = "localhost"; }
let config = import ./config.nix;
in config.port
# 8080

If the imported file is a function, it must be called with its arguments:

# If tools.nix contains { pkgs }: [ pkgs.git pkgs.vim ]
let tools = import ./tools.nix { inherit pkgs; };
in tools

<nixpkgs> is a special path resolved through the channels or the registry. import <nixpkgs> {} returns the gigantic attrset that contains every package.

Builtins: built-in functions

Nix provides built-in functions in the builtins object. Here are the most useful ones:

map, transform a list

map applies a function to every element and returns a new list of the same length. Note that it is used without the builtins. prefix: a few functions such as map, toString and import are exposed directly in the global scope, whereas most of the others require the full prefix.

Terminal window
nix eval --expr 'map (x: x * 2) [ 1 2 3 4 ]'
Result
[ 2 4 6 8 ]

builtins.filter, filter a list

filter keeps the elements for which the function passed as first argument returns true. This function, called a predicate, must return a strict boolean: returning an integer or null triggers an evaluation error. In practice, filter is mostly used to build conditional package lists depending on the platform or the environment.

Terminal window
nix eval --expr 'builtins.filter (x: x > 2) [ 1 2 3 4 5 ]'
Result
[ 3 4 5 ]

Other useful builtins

This table serves as a cheat sheet for the functions you will come across in your first files. Two rows deserve particular attention: toString is written without the prefix builtins., unlike its neighbours, and builtins.hasAttr "a" attrset does exactly the same job as the attrset ? a operator, the functional form being useful when the attribute name is itself computed.

BuiltinUsageExample
builtins.lengthLength of a listbuiltins.length [ 1 2 3 ] → 3
builtins.headFirst elementbuiltins.head [ 10 20 ] → 10
builtins.tailList without the firstbuiltins.tail [ 10 20 30 ] → [ 20 30 ]
builtins.elemMembershipbuiltins.elem 3 [ 1 2 3 ] → true
builtins.attrNamesKeys of an attrsetbuiltins.attrNames { a = 1; } → [ "a" ]
builtins.attrValuesValues of an attrsetbuiltins.attrValues { a = 1; } → [ 1 ]
builtins.hasAttrAttribute testbuiltins.hasAttr "a" { a = 1; } → true
toStringConversion to a stringtoString 42 → "42"
builtins.typeOfType of a valuebuiltins.typeOf 42 → "int"

Putting it all together: anatomy of a shell.nix

Here is how the concepts combine in a typical shell.nix file:

shell.nix
{ pkgs ? import <nixpkgs> {} }: # ← function with a named argument + default
let # ← local variables
projectName = "mon-api";
pythonEnv = pkgs.python312.withPackages (ps: [ # ← lambda
ps.requests
ps.flask
]);
debugMode = true;
debugTools = if debugMode # ← conditional
then [ pkgs.htop pkgs.ncdu ]
else [];
in
pkgs.mkShell { # ← attrset passed to a function
name = "${projectName}-dev"; # ← interpolation
packages = with pkgs; [ # ← with
pythonEnv
git
vim
] ++ debugTools; # ← list concatenation
PROJECT_NAME = projectName; # ← environment variable
}

Each line uses a concept you have just learned. The only new element is pkgs.mkShell, a function provided by nixpkgs that builds a development environment from the attrset you pass it. The guides on development environments and factoring, listed at the bottom of the page, detail what this attrset accepts.

Summary of the concepts

Keep this table at hand during your first readings of nixpkgs files: the Syntax column is what you will recognise on screen, the Description column tells you what it does. The last four rows are the most worthwhile to memorise, because //, ++, import and interpolation appear in almost every derivation.

ConceptSyntaxDescription
Types42, "hello", true, null7 primitive types, everything is an expression
Lists[ 1 2 3 ]Ordered collections, separated by spaces
Attrsets{ key = value; }Dictionaries, the fundamental structure of Nix
Functionsarg: bodyFirst-class values, a single argument
Named args{ a, b ? 0 }: ...Attrset destructuring with optional defaults
let ... inlet x = 1; in xImmutable local variables
withwith pkgs; [ git ]Brings the attributes into scope
inherit{ inherit x; }Shorthand for x = x
if-then-elseif c then a else bConditional expression (else mandatory)
Operator //a // bAttrset merge (right overwrites left)
Operator ++[ 1 ] ++ [ 2 ]List concatenation
importimport ./file.nixLoads and evaluates a file
Interpolation"Hello ${name}"Inserting expressions into strings

Key points

  • Everything is an expression in Nix: there are no statements, every element returns a value.
  • Attrsets ({ key = value; }) are the universal structure, packages, configurations and modules are all attrsets.
  • Functions are everywhere: each .nix file is usually a function that expects pkgs or other arguments.
  • Variables are immutable: once defined in a let, they no longer change.
  • with, inherit and // are syntactic shorthands that make code more readable, they are not mandatory but very common.
  • The REPL (nix repl) and nix eval --expr are your best allies to experiment.

Knowledge check

Check that the essentials of this guide are acquired. The questions only cover what has just been explained here.

Knowledge check

Check what you have learned with this interactive quiz

6 questions
6 min
70% to pass

How it works

  • The timer starts when you click Start
  • Multiple choice, true/false and short answer questions
  • You can move between questions
  • Detailed results are shown at the end

Starts the quiz and the timer

Is this project useful to you?

The guides, courses, quizzes and labs stay free. If you use them regularly, you can contribute to their upkeep and development.

Become a supporting member (new tab) One-time support (new tab) Learn more

Follow the project on LinkedIn