Writing \usepackage{expl3} on a current LaTeX loads nothing at all. expl3 — the programming layer that grew out of the effort once called “LaTeX3” — is already part of the format. Line 1125 of the kernel source latex.ltx logs exactly that: Skipping: expl3 code already part of the format, and expl3.sty checks at line 53 whether the code is already present and, if it is, throws the whole load away. This page measures what \ExplSyntaxOn really does to the reading of characters, then works through how to read a name like \module_function:nn, what the argument specifiers mean, and the data types — tl, seq, prop, int, fp and the rest.
expl3 is already in the kernel — \fpeval is the proof
TeX began as a macro processor, a tool for defining commands with \def and \newcommand. But writing a large package exposed how inconsistent the raw primitives are: controlling expansion and handling variables became a craft. expl3 gives the TeX and e-TeX primitives new names, names functions and variables systematically, and writes each argument’s type into the name itself. Built up over many years by the LaTeX Project, it amounts to both a standard library and a programming language for LaTeX — xparse, siunitx, fontspec and l3keys2e all stand on it.
And that layer is no longer sold separately. Open expl3.sty and lines 53 to 59 test whether \tex_let:D is already defined; if it is, \@gobble eats the \input expl3-code.tex whole. What survives as a package is a name-only shell — it even announces itself as L3 programming layer (loader) — while the substance sits in the format. Sure enough, in a bare article with nothing loaded, \ExplSyntaxOn, \tl_new:N and \ProvidesExplPackage are all already defined. The clearest proof is four lines starting at latex.ltx line 1167: \fpeval, \inteval, \dimeval and \skipeval are defined with \cs_new_eq:NN as aliases of \fp_eval:n, \int_eval:n, \dim_eval:n and \skip_eval:n. The document-level \fpeval{sqrt(2)} is therefore the expl3 function itself, called by another name (see the counters and lengths page).
What \ExplSyntaxOn actually changes: space and ~ swap roles
expl3 code lives between \ExplSyntaxOn and \ExplSyntaxOff, and inside that region exactly four characters change category code. Ask the kernel to print \the\catcode before and after and you get this: space goes from 10 (space) to 9 (ignored character), _ from 8 (subscript) to 11 (letter), : from 12 (other) to 11 (letter), and ~ from 13 (active) to 10 (space). \ExplSyntaxOff restores 10, 8, 12 and 13 exactly. Category codes themselves are the subject of the catcode page, but what is delightful here is that space and ~ have swapped seats. Space gives up catcode 10 and moves to 9, the “ignored” slot, and ~ sits down in the vacated 10. Which is why, inside expl3, ~ is not a command that produces a space — it literally is a space character.
| Character | Ordinary LaTeX | Inside ExplSyntaxOn |
|---|---|---|
(space) | 10 — space | 9 — ignored character. Indentation and line breaks never reach the output |
~ | 13 — active (a non-breaking space) | 10 — a space itself. This is what you write when the output needs a gap |
_ | 8 — subscript | 11 — letter. It may appear inside a command name |
: | 12 — other | 11 — letter. It can separate the argument signature |
The practical consequence of that swap is a single rule: a bare space inside expl3 never reaches the output. To typeset “Fruit: apple” you must write Fruit:~#1; replace the ~ with a plain space and you get the run-together “Fruit:apple”. Turned around, it means you can indent your code as much as you like and put gaps between tokens for readability without changing the output at all. Given how much attention ordinary LaTeX demands about spaces, that is quite a liberation.
% no \usepackage{expl3} is needed: it is in the format
\ExplSyntaxOn
% spaces and newlines here are catcode 9 (ignored); _ and : are letters
\tl_new:N \l_greeting_tl
\tl_set:Nn \l_greeting_tl { Hello,~world! } % ~ is the real space
\tl_use:N \l_greeting_tl
\ExplSyntaxOffHow to read \seq_put_right:Nn — the name states the types
An expl3 function name has the form \⟨module⟩_⟨description⟩:⟨arg-signature⟩. Everything up to the first _ is the module (a data type or area of work); everything up to the : is the descriptive name; what follows the : is the argument signature. In \seq_put_right:Nn the module is seq (sequences), the description is put_right (append on the right), and the signature is Nn. Each letter of the signature says how that argument is processed before it is passed on. Read the name and you know how many arguments it takes and what happens to each — reading the types without consulting the manual is precisely what the scheme is for.
| Specifier | Meaning |
|---|---|
N | No manipulation; a single token (usually one control sequence). |
n | No manipulation; a braced group of tokens. |
c | Turns the argument into a control sequence via \csname before use. |
V / v | Passes the value of a variable (V from a single token, v builds the name first). |
o | Expands the argument once before use. |
x / e | Full expansion (x is like \edef and is not expandable; e uses \expanded). |
f | Expands left to right, up to the first unexpandable token. |
p | A TeX parameter text (#1#2…); used when defining a function. |
T / F | Code to run when the test is true / false. Usually paired as TF, as in \tl_if_empty:nTF, with T-only and F-only variants also provided. |
Variables follow the same style but begin with a single letter for scope. l_ is local (changed only within the current TeX group), g_ is global, c_ is a constant. A type identifier ends the name: _tl (token list), _int (integer), _seq (sequence), _prop (property list), _clist (comma list), _fp (floating point), _str (string), _bool (boolean), and so on. So \l_my_name_tl reads at a glance as “a local token-list variable” and \g_counter_int as “a global integer.” Each module also provides scratch variables — disposable temporaries — such as \l_tmpa_tl and \l_tmpb_int.
Seeing the difference between :Nn and :Nx
n stores what you wrote; x burns in the value as it stands right now. Rather than explain that abstractly, dump the contents to the log with \tl_show:N. Put the same { [ \l_src_tl ] } into two variables, one with \tl_set:Nn and one with \tl_set:Nx, and the log reads: > \l_a_tl=[\l_src_tl ]. and > \l_b_tl=[FIRST]. The n version holds the token \l_src_tl itself; the x version has already become FIRST, the value at assignment time. Now change \l_src_tl to SECOND and typeset both: the n version prints “[SECOND]”, the x version “[FIRST]”. In other words, n keeps a reference and x keeps a snapshot. Most of the situations people describe as “expansion control is hard” are really this choice made the wrong way round. \tl_show:N is worth remembering as the first move when you get stuck.
\ExplSyntaxOn
\tl_new:N \l_src_tl \tl_set:Nn \l_src_tl { FIRST }
\tl_new:N \l_a_tl \tl_set:Nn \l_a_tl { [ \l_src_tl ] } % reference
\tl_new:N \l_b_tl \tl_set:Nx \l_b_tl { [ \l_src_tl ] } % snapshot
\tl_show:N \l_a_tl % log: > \l_a_tl=[\l_src_tl ].
\tl_show:N \l_b_tl % log: > \l_b_tl=[FIRST].
\tl_set:Nn \l_src_tl { SECOND }
Nn~stored:~\tl_use:N \l_a_tl \par % prints [SECOND]
Nx~stored:~\tl_use:N \l_b_tl \par % prints [FIRST]
\ExplSyntaxOffThe data types: create, set, use
expl3 is split into modules by data type, and they all share the same rhythm: create, set, use. Declare with \⟨type⟩_new:N, put a value in with \⟨type⟩_set:Nn, get it out with \⟨type⟩_use:N or a sibling. The gestures do not change as the type changes, so learning one lets you guess the rest. Only function definition looks a little different: \cs_new:Npn defines a new function and raises an error if one of that name already exists (cs is “control sequence”). \cs_set:Npn also defines one, but only within the current TeX group, and does not complain about redefinition. Both use :Npn, where N is the function being defined, p its parameter text (#1#2…) and n the body, the replacement text.
| Module | What it holds | Common functions, with measured results |
|---|---|---|
tl | Token list — the most basic variable, usable much like a string | \tl_new:N / \tl_set:Nn (the old contents go) / \tl_use:N / \tl_show:N |
str | Strings — for comparing as characters rather than tokens | \str_if_eq:nnTF { a~b } { a~b } returns same |
int | Integers — integer arithmetic with the usual operators | \int_eval:n { 2 + 3 * 4 } is 14 (\inteval is its alias) |
fp | Floating point — with scientific functions such as sin, sqrt, pi | \fp_eval:n { sqrt(2) } is 1.414213562373095; \fp_eval:n { 2 * pi } is 6.283185307179586 |
seq | Sequences — lists with access at both ends (also usable as stacks) | \seq_put_right:Nn appends, \seq_map_inline:Nn walks every item as #1, \seq_use:Nn joins with a separator |
prop | Property lists — dictionaries, mapping keys to values | \prop_put:Nnn stores variable, key, value; \prop_item:Nn retrieves |
clist | A comma-separated list of values | After \clist_set:Nn \l_c_clist { 1,2,3 }, \clist_use:Nn \l_c_clist { ~+~ } gives “1 + 2 + 3” |
bool | Booleans — what you use instead of \newif | \bool_new:N / \bool_set_true:N / \bool_if:NTF to branch |
\documentclass{article}
\begin{document}
\ExplSyntaxOn
\seq_new:N \l_example_fruits_seq
% define a function that appends one item
\cs_new:Npn \example_add:n #1
{ \seq_put_right:Nn \l_example_fruits_seq {#1} }
\example_add:n { apple }
\example_add:n { banana }
\example_add:n { cherry }
% each item arrives as #1; ~ is a real space
\seq_map_inline:Nn \l_example_fruits_seq
{ Fruit:~#1 \par }
\ExplSyntaxOff
\end{document}Compile that and “Fruit: apple”, “Fruit: banana” and “Fruit: cherry” come out on three separate lines. The #1 in \cs_new:Npn is the defined function’s argument, while the #1 in \seq_map_inline:Nn is each item being mapped over — both arrive as n-type, the braced contents. The _ and : in the command names cause no trouble precisely because this is an \ExplSyntaxOn region.
l3keys, and how expl3 is used when writing a package
When you want key = value options on your own commands or package, the standard tool is l3keys. Declare keys with \keys_define:nn { module } { ... } and set them with \keys_set:nn { module } { key = value }. A key declaration carries a trailing property such as .tl_set:N (store into a token-list variable), .bool_set:N (make it a boolean), .code:n (run arbitrary code) or .initial:n (a default value). This machinery was originally wired to package options through a package called l3keys2e, but its core has since been absorbed into the LaTeX2ε kernel and is available without any package as \DeclareKeys and \ProcessKeyOptions (the package-authoring page writes one out).
For ordinary document writing you will hardly ever need to write expl3 directly. But the moment you set out to write a package or a class, expl3 is now effectively the standard. A common pairing is to take the user-facing command with \NewDocumentCommand (xparse) and implement its body in expl3. Keep two things apart: xparse’s argument specification (document-level arguments such as m, O{...}, s) and expl3’s argument signature (programming-level processing such as N, n) are not the same thing. The former is covered in detail on the xparse page. And if the first line of your .sty is \ProvidesExplPackage, expl3 syntax switches on automatically from that point — you do not write \ExplSyntaxOn at all.
- Wrap code in
\ExplSyntaxOn…\ExplSyntaxOff. Inside, a bare space becomes category code 9 (ignored); use~when the output needs a gap. - No
\usepackage{expl3}is needed — it is in the kernel, andexpl3.stydiscards the load itself. In a.sty,\ProvidesExplPackageremoves the need for\ExplSyntaxOntoo. - Functions are
\⟨module⟩_⟨description⟩:⟨signature⟩; variables are\⟨scope⟩_⟨name⟩_⟨type⟩, with scopel_/g_/c_. - Do not mix up
nandx. Choose according to whether you want to keep a reference or burn in the current value. When unsure, print it with\tl_show:Nand look. - Do not invent command names. The naming is strict; the official interface3 manual (
texdoc interface3) is the primary source.