Category codes & \makeatletter

The dollar sign has no power of its own to open mathematics. Hand that job to ! instead — !x_1! is then typeset as mathematics without a single error — because what decides the role is not the character but its category code (catcode), the number from 0 to 15 that TeX attaches to every character it reads. That one number explains why LaTeX’s internal commands are full of @, what \makeatletter actually does, and why % swallows the rest of a line without a word of complaint. This page walks through all sixteen codes, the \catcode primitive that rewrites them, the one-line reality of \makeatletter, and how to inspect any token with \string, \meaning and \detokenize.

The sixteen category codes and which character gets which

There are sixteen category codes, numbered 0 to 15, and TeX uses them in the stage where it cuts your source into tokens — lexical analysis, long before any typesetting happens. Whether a character starts a command, opens a group, or is merely ink is settled by this number alone. In daily work you notice 0 (starts a command), 1 and 2 (grouping), 3 (mathematics), 4 (the cell separator in tables), 5 (the line ending that builds paragraphs), 6 (a macro’s argument marker), 7 and 8 (super- and subscripts), the trio 10, 11, 12 that makes up running text, and 14 (comments).

catcodeRoleDefault characters (inside a LaTeX document)
0Escape — starts a commandBackslash \
1Begin groupLeft brace {
2End groupRight brace }
3Math shiftDollar sign $
4Alignment tab — the cell separatorAmpersand &
5End of line — a blank line makes a paragraphCarriage return (character 13)
6Parameter — a macro argumentHash #
7SuperscriptCaret ^
8SubscriptUnderscore _
9Ignored — skipped as if absentno character at all by default in LaTeX
10SpaceSpace and tab (character 9)
11Letter — may form a command nameaz, AZ
12Other — plain inkDigits, punctuation, @, and everything else
13Active — the character is itself a commandTilde ~ and control characters 1–31
14Comment — discards the rest of the linePercent %
15Invalid — raises an errorNull (character 0) and delete (character 127)

Read the row for 9 carefully. Most accounts say “catcode 9 is the null character,” but that describes bare TeX (IniTeX). Scan all 256 characters in LaTeX under TeX Live 2024 and not one of them has catcode 9: latex.ltx moves the null character to 15 (invalid) and leaves 9 empty — the only one of the sixteen categories with no default inhabitant. It does nothing until you assign a character to it yourself. The listings package does exactly that, dropping tab, form feed and carriage return to catcode 9 while it reads its own source so that they are skipped. At the other end, control characters 1–31 being active (13) is modern LaTeX’s machinery for handling UTF-8 input.

Rewriting a character’s role with \catcode

The mapping from characters to categories is not fixed: the primitive \catcode rewrites it. The form is “\catcode, then a backtick, then the target character, then =, then the new number,” and \the\catcode followed by the same backtick-and-character reads the current value back. The code below hands the math-shift job (catcode 3) to !. There are zero errors, and both !x_1! and !\frac{a}{b}! come out as proper mathematics. $ opens mathematics not “because it is a dollar sign” but “because it has catcode 3” — and the charm of this machinery is that one line proves it.

document.tex
\documentclass{article}
\begin{document}
\catcode`\!=3          % hand the math-shift job to "!"
!x_1! and !\frac{a}{b}!  % typeset as mathematics, zero errors
\catcode`\!=12         % give it back to "other"
Back to normal!

% read a catcode back
\the\catcode`\$        % 3
\the\catcode`\%        % 14
\the\catcode`A         % 11
\end{document}

It is, however, a tool that works too well. Categories are frozen at tokenization, so from the moment you rewrite one, everything read afterwards changes meaning; forget to restore it and something entirely unrelated breaks. The change is also undone when a group ({ } or an environment) closes, which produces its own confusion — “it worked in the preamble but not in the body.” Treat a raw \catcode as the last resort for cases that do not fit inside a settled wrapper such as the \makeatletter below. Packages do this routinely, though: load babel with its German option and " becomes catcode 13 (active), so "a gives ä and a double quote followed by a backtick gives „. That is why quotation marks suddenly behave differently in a German document.

Why % silently eats a line and & raises an error

The answer is the catcode value itself. % has catcode 14 — “discard everything from here to the end of the line” — and it never reports what it discarded. So Only 50% of the sample survived. followed on the next line by The rest did not. prints Only 50The rest did not. with zero errors and zero warnings. A line of your prose is gone and the log is perfectly silent. This is one of the hardest failures in all of LaTeX to spot; always escape a percent sign in running text as \%. By contrast & has catcode 4 (the table cell separator), so writing one outside a table earns an immediate ! Misplaced alignment tab character &. And $ has catcode 3, which is why leaving one unclosed gives ! Missing $ inserted.

document.tex
% catcode 14: everything after % on this line is discarded, silently
Only 50% of the sample survived.
The rest did not.
% output: "Only 50The rest did not."  -- no error, no warning

% catcode 4 outside a table:
Smith & Sons   % ! Misplaced alignment tab character &.

% the fix in running text:
Only 50\% of the sample survived.

Catcode 11 decides where a command name ends

A TeX control word — a named command such as \section — is recognized as a catcode-0 character (usually \) followed by a run of catcode-11 characters. Only catcode-11 characters can normally form a command’s name, which is exactly why \section ends at section: the next character, a space or a {, is not catcode 11. Digits and punctuation are catcode 12 (other), so \a2 reads as “the command \a, then the character 2.” Turn that around and you get the key move: change a character’s catcode to 11 and it becomes usable inside command names. The story of @ in the next section starts with exactly that. (The user-level consequences — the space that vanishes after a control word, \LaTeXlogo coming out undefined — belong to the “Syntax rules” page.)

What \makeatletter is: one line of code

\makeatletter does nothing but set the catcode of @ to 11, and its partner \makeatother does nothing but set it back to 12. In latex.ltx each definition is literally one line, and the names say it: make @ a letter, make @ other. Why is that needed? Because LaTeX’s kernel and packages are full of internal commands whose names contain @\@startsection (assembling a section heading), \@ifnextchar (peeking at the next token to branch), \@maketitle (the title block), and hundreds more. While \usepackage and \documentclass read a .sty or .cls file they switch @ to catcode 11 for you, so inside a package those names parse as single commands.

latex.ltx
% the whole of \makeatletter and \makeatother, in the LaTeX kernel
\DeclareRobustCommand\makeatletter{\catcode`\@11\relax}
\DeclareRobustCommand\makeatother{\catcode`\@12\relax}

% two abbreviations you will meet in internal code (note the values)
\newdimen\p@ \p@=1pt   % 1pt -- "this saves macro space and time"
\newdimen\z@ \z@=0pt   % 0pt, and doubles as the integer 0

Inside an ordinary document @ has catcode 12 (other). Write \p@ in your text or preamble and TeX reads “the command \p, then the character @,” and stops with ! Undefined control sequence. The error display breaks at l.3 Value: \p and resumes on the next line with @ — you can literally see where the command name was cut off, so this symptom is worth memorising for fast diagnosis. Incidentally \p@ is 1pt; \z@ is the one that means 0pt, and confusing the two is a common slip. The comment still sitting in latex.ltx — that the abbreviation “saves macro space and time” — is a relic of an era when memory was scarce, running unchanged to this day.

error
! Undefined control sequence.
l.3 Value: \p
             @
The control sequence at the end of the top line
of your error message was never \def'ed.

When you need \makeatletter, and when you must not write it

The practical rule fits in one sentence: you need it only inside a .tex document (almost always the preamble) when you write an internal command whose name contains @. Nowhere else. In particular, do not write it inside a .sty or .cls@ is already catcode 11 while those are being read, so it is unnecessary, and a stray \makeatother can break what follows. The typical use is a light adjustment, in the preamble, to an internal macro your class defined. The example below redefines \@maketitle, the macro that lays out the title block, inside the wrapper.

document.tex
\documentclass{article}

\makeatletter                 % @ becomes a letter here
\renewcommand{\@maketitle}{%   % redefine the internal title block
  \begin{center}
    {\LARGE\bfseries \@title}\par
    \vspace{1ex}{\large \@author}\par
  \end{center}%
}
\makeatother                  % ... and goes back to "other" here

\title{Category codes}
\author{A. Author}
\begin{document}
\maketitle
\end{document}

There are two traps. The first is forgetting \makeatother: leave it off and @ stays a letter into everything that follows, breaking an email address in your text or any package that treats @ specially. Make it mechanical — open with one, close with the other. The second is not checking whether you can avoid touching internals at all. If a public command can be overridden with \renewcommand, or a proper package already provides what you want, that route is always safer. Internal commands change without notice when a package is updated, and a redefinition you wrote inside \makeatletter will quietly stop working at the next release.

Why \verb and listings have to touch catcodes

Printing source exactly as written leaves only one option: stop the special characters from being special. That is literally what \verb does, and the verbatim environment does the same over a whole block. It applies \@makeother to the eleven characters listed in \dospecials — space, \, {, }, $, &, #, ^, _, %, ~ — dropping every one of them to catcode 12 (other). Then \@noligs goes the other way and makes six more catcode 13 (active): the backtick, <, >, ,, the apostrophe and -, so that two backticks are not quietly turned into a curly opening quote. \verb therefore both lowers and raises catcodes, and because the whole thing sits inside a \bgroup, everything reverts automatically when it ends.

latex.ltx
\def\@makeother#1{\catcode`#1=12\relax}
\def\dospecials{\do\ \do\\\do\{\do\}\do\$\do\&%
  \do\#\do\^\do\_\do\%\do\~}

\def\verb{\relax\ifmmode\hbox\else\leavevmode\null\fi
  \bgroup                                  % everything below is local
    \verb@eol@error \let\do\@makeother \dospecials   % all 11 -> catcode 12
    \verbatim@font\@noligs                 % ` < > , ' - -> catcode 13
    \language\l@nohyphenation
    \@ifstar\@sverb\@verb}

From this follows \verb’s famous restriction: \verb cannot be used inside another command’s argument. The argument is tokenized before the macro is even called, so by the time \verb changes any catcode it is far too late. \footnote{code: \verb|\foo_bar|} produces ! Undefined control sequence. (because \foo was read as a real command) followed by ! Missing $ inserted. (because _ was read as a subscript), and inside a macro argument you also get ! Extra }, or forgotten $. The way out is \lstinline from listings: \section{A \lstinline|x_1| heading} compiles with zero errors and even survives into the table of contents. The forty-odd catcode manipulations inside listings exist precisely to absorb problems of this kind.

document.tex
% BREAKS: the argument is tokenized before \verb can act
\footnote{code: \verb|\foo_bar|}
%   ! Undefined control sequence.   <argument> ... \verb |\foo
%   ! Missing $ inserted.

% WORKS: \lstinline survives inside a moving argument
\usepackage{listings}
\section{A \lstinline|x_1| heading}   % zero errors, reaches the ToC

Looking inside a token: \string, \meaning, \detokenize

If category codes still feel abstract, the fastest cure is to look at them. \meaning states the catcode in words: \meaning A gives the letter A (catcode 11, hence “letter”), \meaning 7 gives the character 7 (catcode 12), and \meaning\bgroup gives begin-group character {. One phrase tells you what a character currently is. \string unpacks a command into its characters, backslash included, and \detokenize turns a whole argument into printable text — the # that comes out as ## is doubling itself to stay faithful as a token list. For debugging, \show is handy too: it writes a definition to the log and pauses.

CommandWhat it doesMeasured output under TeX Live 2024
\the\catcodeReads a character’s catcode as a number\the\catcode + backtick + $ gives 3
\meaningDescribes a token in words\meaning A gives the letter A; \meaning 7 gives the character 7
\stringUnpacks a command into characters, backslash included\string\frac gives \frac
\detokenizeTurns a whole argument into printable text\detokenize{\frac{1}{2} #1} gives \frac {1}{2} ##1
\showWrites a definition to the log and pauses\show\LaTeX gives macro:->\protect \LaTeX

One last piece of practical instinct. When you hit a bug of the form “only this one character misbehaves,” check that character’s number with \the\catcode first. Packages such as babel, csquotes, listings and hyperref all make particular characters active or drop them to catcode 12 for their own purposes. If the symptom is confined to a single character, the cause is a category code nine times out of ten. Once you know that, the fix narrows to three options: change the load order, avoid the character, or use the escape hatch the package already provides (\%, a different delimiter for \verb, \lstinline).