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,inheritandif-then-else - Transform data with the builtins (
map,filter,attrNames…) - Write complete expressions ready for
shell.nixorflake.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.nixorflake.nixfiles 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:
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:
nix eval --expr '1 + 2'3Every 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
nix eval --expr '1 + 2'3nix eval --expr '7 / 2'3Division between integers is integer division (no rounding, truncation). To get a decimal result:
nix eval --expr '7.0 / 2.0'3.5nix eval --expr 'builtins.typeOf 42'"int"Strings
Strings are delimited by double quotes:
nix eval --expr '"hello" + " world"'"hello world"The + operator concatenates strings (and adds numbers, Nix tells them apart
by the type of the operands).
Interpolation with ${}:
nix eval --expr 'let name = "Nix"; in "Hello ${name}"'"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.
nix eval --expr 'true && false'falsenix eval --expr 'true || false'trueComparison operators: ==, !=, <, >, <=, >=.
nix eval --expr '3 > 2'trueNull 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 pathSummary 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").
| Type | Examples | builtins.typeOf |
|---|---|---|
| Integer | 42, -1, 0 | "int" |
| Float | 3.14, 7.0 | "float" |
| String | "hello", ''multi'' | "string" |
| Boolean | true, false | "bool" |
| Null | null | "null" |
| Path | /tmp, ./file.nix | "path" |
| Function | x: 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 allowedList 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.
nix eval --expr 'builtins.length [ 1 2 3 ]'3nix eval --expr 'builtins.head [ 10 20 30 ]'10nix eval --expr 'builtins.tail [ 10 20 30 ]'[ 20 30 ]nix eval --expr '[ 1 2 ] ++ [ 3 4 ]'[ 1 2 3 4 ]The ++ operator concatenates two lists. builtins.elem tests membership:
nix eval --expr 'builtins.elem 3 [ 1 2 3 ]'trueAttribute 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.
nix eval --expr '{ a = 1; b = 2; }.a'1Merging with //
The // operator merges two attrsets. The keys on the right-hand side
overwrite those on the left:
nix eval --expr '{ a = 1; b = 2; } // { b = 99; c = 3; }'{ 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.
nix eval --expr 'builtins.attrNames { z = 1; a = 2; m = 3; }'[ "a" "m" "z" ]The names are returned sorted alphabetically.
nix eval --expr 'builtins.attrValues { a = 10; b = 20; }'[ 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.
nix eval --expr '{ a = 1; } ? a'truenix eval --expr '{ a = 1; } ? b'falseCombined 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
nix eval --expr '(x: x * 2) 5'10The 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:
nix eval --expr '(a: b: a + b) 3 4'7a: 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:
nix eval --expr '({ name, age }: "${name} a ${toString age} ans") { name = "Alice"; age = 30; }'"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:
nix eval --expr 'let x = 5; y = 3; in x + y'8The variables can reference each other:
let base = 10; double = base * 2; triple = base * 3;in double + triple# 50with, bringing a scope in
The with keyword brings all the attributes of an attrset into the current
scope:
nix eval --expr 'let s = { a = 1; b = 2; }; in with s; a + b'3Typical use, avoiding repeating pkgs.:
# Without withpackages = [ pkgs.git pkgs.vim pkgs.ripgrep ];
# With withpackages = with pkgs; [ git vim ripgrep ];inherit, assignment shorthand
inherit copies variables from the enclosing scope into an attrset:
nix eval --expr 'let x = 1; y = 2; in { inherit x y; }'{ 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:
nix eval --expr 'if 3 > 2 then "oui" else "non"'"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 ++ debugToolsimport, 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# 8080If 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.
nix eval --expr 'map (x: x * 2) [ 1 2 3 4 ]'[ 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.
nix eval --expr 'builtins.filter (x: x > 2) [ 1 2 3 4 5 ]'[ 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.
| Builtin | Usage | Example |
|---|---|---|
builtins.length | Length of a list | builtins.length [ 1 2 3 ] → 3 |
builtins.head | First element | builtins.head [ 10 20 ] → 10 |
builtins.tail | List without the first | builtins.tail [ 10 20 30 ] → [ 20 30 ] |
builtins.elem | Membership | builtins.elem 3 [ 1 2 3 ] → true |
builtins.attrNames | Keys of an attrset | builtins.attrNames { a = 1; } → [ "a" ] |
builtins.attrValues | Values of an attrset | builtins.attrValues { a = 1; } → [ 1 ] |
builtins.hasAttr | Attribute test | builtins.hasAttr "a" { a = 1; } → true |
toString | Conversion to a string | toString 42 → "42" |
builtins.typeOf | Type of a value | builtins.typeOf 42 → "int" |
Putting it all together: anatomy of a shell.nix
Here is how the concepts combine in a typical shell.nix file:
{ 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 [];inpkgs.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.
| Concept | Syntax | Description |
|---|---|---|
| Types | 42, "hello", true, null | 7 primitive types, everything is an expression |
| Lists | [ 1 2 3 ] | Ordered collections, separated by spaces |
| Attrsets | { key = value; } | Dictionaries, the fundamental structure of Nix |
| Functions | arg: body | First-class values, a single argument |
| Named args | { a, b ? 0 }: ... | Attrset destructuring with optional defaults |
| let ... in | let x = 1; in x | Immutable local variables |
| with | with pkgs; [ git ] | Brings the attributes into scope |
| inherit | { inherit x; } | Shorthand for x = x |
| if-then-else | if c then a else b | Conditional expression (else mandatory) |
| Operator // | a // b | Attrset merge (right overwrites left) |
| Operator ++ | [ 1 ] ++ [ 2 ] | List concatenation |
| import | import ./file.nix | Loads 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
.nixfile is usually a function that expectspkgsor other arguments. - Variables are immutable: once defined in a
let, they no longer change. with,inheritand//are syntactic shorthands that make code more readable, they are not mandatory but very common.- The REPL (
nix repl) andnix eval --exprare 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
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
Review
(0/0)Score by domain
A domain groups several guides and gets several questions, so its score is more reliable than a per-topic score, which often rests on a single question.
Summary
Strengths
No topic passed at 80% on at least two questions yet.
Weak areas
No topic below the pass mark.
Recommended next
Hints before you try again?
Continue the course: Write a first NixOS configuration
A new full quiz with random questions
Work again only on the questions you missed