Open a LaTeX package distribution and you may find no .sty at all. What is there instead is a pair of files, .dtx and .ins — an implementation of literate programming in which the code and its commentary live in one file. Run booktabs’s .dtx through docstrip and it reports Lines processed: 1053 / Comments removed: 882 / Codelines passed: 161. Of 1053 lines, only 161 are actually code; the other 85 % is prose. Feed the same file to pdflatex and that prose becomes a typeset PDF manual. This page runs from gathering the macros you keep pasting into your preamble into a .sty of your own, through writing a .dtx, testing it, and getting it onto CTAN.
The .sty skeleton — writing a date buys you version checking
A package is a .sty file, sitting in your project or installed somewhere. Its first two lines are self-identification: \NeedsTeXFormat{LaTeX2e} states the format required and \ProvidesPackage{name}[date version description] declares the package name and release. The name must match the file’s basename. The bracketed part is optional, but writing it buys something concrete: users can then demand a minimum date with \usepackage{mypackage}[2027/01/01], and an older copy produces LaTeX Warning: You have requested, on input line 2, version '2027/01/01' of package mypackage, but only version '2026/08/17 v1.0 ...' is available. Write the date in YYYY/MM/DD form. That one line removes one “it just doesn’t work” email a few years from now.
In the body, pull in dependencies with \RequirePackage{...} — the .sty equivalent of \usepackage. Load xcolor for colour, tikz for drawing, and so on. To tell the user something, use \PackageWarning{name}{message}, or \PackageError{name}{message}{help} when you cannot continue. Both take the package name as their first argument, so whoever reads the log can see at a glance where the message came from. The machinery for accepting \usepackage[option]{name} — \DeclareOption with \ProcessOptions, and the \DeclareKeys / \ProcessKeyOptions pair that the current kernel provides — is shared with classes and is gathered on the class page. The steps are identical when you are writing a package. The kvoptions package you will meet in older .sty files is an earlier generation of the same idea.
\NeedsTeXFormat{LaTeX2e}
\ProvidesPackage{mypackage}[2026/08/17 v1.0 My helpers]
\RequirePackage{xcolor}
\newcommand{\greet}[1][world]{Hello, \textcolor{blue}{#1}!}
\endinputWriting your own .dtx and .ins — what %<*package> means
The trick behind .dtx is almost embarrassingly simple: a % at the start of a line separates commentary from code. Lines beginning with % are prose; the rest is code. That is why one file can be read two ways. Run tex mypackage.ins and docstrip throws the prose away and writes out the .sty; run pdflatex mypackage.dtx and the prose is typeset as the body while the code is quoted with line numbers. Which stretches get extracted is marked by guards, %<*package> and %</package>, and the .ins line \generate{\file{mypackage.sty}{\from{mypackage.dtx}{package}}} says “write whatever is inside the package guard into mypackage.sty”. Guard names are yours to choose, so one .dtx can generate a .sty, a .cls and a configuration file together. Whatever you put between \preamble and \endpreamble is prepended as a comment to every generated file — that is where the licence notice goes.
% \iffalse meta-comment
% Copyright (C) 2026 Example Author
% This work may be distributed and/or modified under the conditions of
% the LaTeX Project Public License, version 1.3c or later.
% \fi
%
% \iffalse
%<*driver>
\documentclass{ltxdoc}
\usepackage{mypackage}
\EnableCrossrefs
\CodelineIndex
\begin{document}
\DocInput{mypackage.dtx}
\PrintIndex
\end{document}
%</driver>
%<package>\NeedsTeXFormat{LaTeX2e}
%<package>\ProvidesPackage{mypackage}
%<package> [2026/08/17 v1.0 A demonstration package]
% \fi
%
% \title{The \textsf{mypackage} package}
% \author{Example Author}
% \maketitle
%
% \section{Usage}
% \DescribeMacro{\greet}
% |\greet| prints a greeting; the optional argument sets the name.
%
% \StopEventually{}
%
% \section{Implementation}
% \begin{macrocode}
%<*package>
\RequirePackage{xcolor}
\newcommand{\greet}[1][world]{Hello, \textcolor{blue}{#1}!}
%</package>
% \end{macrocode}
% \Finale
\endinputThe commentary side needs only a handful of commands. \DocInput{file} is the workhorse that reads the .dtx back in, and the stretch fenced by %<*driver> … %</driver> is the small document setup that does it (using ltxdoc). The implementation is quoted inside \begin{macrocode} … \end{macrocode}, by convention with four spaces of indentation on the fence lines. \DescribeMacro{\command} highlights a command in the user-facing text and registers it in the index. \StopEventually{} marks the boundary where the implementation begins, which matters when you generate a shorter user-only version. Finally \Finale tidies up the index and change history. Run the .dtx above with a six-line .ins and you get Lines processed: 41 / Comments removed: 24 / Codelines passed: 10, and the generated .sty opens with the automatic note %% This is file 'mypackage.sty', generated with the docstrip utility.
\input docstrip.tex
\keepsilent
\preamble
Generated from mypackage.dtx -- do not edit this file directly.
\endpreamble
\generate{\file{mypackage.sty}{\from{mypackage.dtx}{package}}}
\endbatchfileA .dtx rots on its own — booktabs no longer typesets
There is a pitfall worth knowing before you write one: the .sty can keep working while the .dtx stops typesetting. On TeX Live 2024, pdflatex booktabs.dtx throws 127 errors and produces no PDF at all. The first is ! Improper alphabetic constant, and it dies around the \CharacterTable. Meanwhile booktabs.sty itself works perfectly. The cause is that the ground under the documentation shifted: doc.sty in TeX Live 2024 is v3.0m, dated 2022/11/13 — Frank Mittelbach’s rewritten “V3” — while booktabs.dtx still dates from January 2020. The booktabs.pdf shipped in the distribution carries that same January 2020 date and has never been regenerated since. For contrast, on the same TeX Live 2024, multirow.dtx (30 pages), tabularx.dtx (12 pages) and array.dtx (35 pages) all typeset without a warning, so this is not a flaw in the mechanism but a maintenance problem. Actually build your own .dtx at every release.
You need not start from a blank page. TeX Live ships an introduction called dtxtut, and with it a template pair, skeleton.dtx and skeleton.ins. Traces of packages that began by copying it survive in unexpected places: line 18 of skeleton.ins reads \usedir{tex/latex/skeleton}, and the identical line still sits at line 36 of booktabs.ins — the template’s skeleton was never renamed. Searching all 1402 .ins files under source/latex in TeX Live 2024, booktabs is the only one still carrying that leftover. It is an endearing detail, but the lesson is plain: if you start from the template, grep for the template’s name before you ship.
Writing in expl3: \ProvidesExplPackage
If the body is going to be expl3, swap the identification line for \ProvidesExplPackage{name}{date}{version}{description}. Besides splitting into four arguments, it has one more important property: the command ends by running \ExplSyntaxOn. So from the line after the declaration onwards, expl3 syntax is available without your ever writing \ExplSyntaxOn. The .sty below contains not one occurrence of it, yet both \tl_new:N and \NewDocumentCommand work as written. How to read expl3 itself is on the expl3 page.
\NeedsTeXFormat{LaTeX2e}
% four arguments, and it turns on expl3 syntax by itself
\ProvidesExplPackage{expldemo}{2026/08/17}{1.0}{Expl demo}
\tl_new:N \l_expldemo_tl
\tl_set:Nn \l_expldemo_tl { from~expl3 }
\NewDocumentCommand \shout { } { \tl_use:N \l_expldemo_tl }Testing and building the distribution: l3build
Instead of typing tex mypackage.ins and pdflatex mypackage.dtx by hand every time, you can hand the job to l3build, maintained by the LaTeX Project (the copy in TeX Live 2024 is the 2024-02-08 release). Put a build.lua at the root of the project listing the module name and its files, and l3build unpack runs the .ins and puts the .sty in build/unpacked/, while l3build doc typesets the .dtx into a PDF under build/doc/. The advantage is that nothing generated litters your working directory. l3build check is the test harness: it matches .lvt test documents against .tlg files of expected log output in testfiles/ and shows you a diff whenever the output changes. Once typeset results are guarded by machine rather than by eye, refactoring stops being frightening.
module = "mypackage"
sourcefiles = {"mypackage.dtx", "mypackage.ins"}
installfiles = {"mypackage.sty"}
uploadconfig = { pkg = "mypackage" }
-- l3build unpack -> build/unpacked/mypackage.sty
-- l3build doc -> build/doc/mypackage.pdf
-- l3build check -> run testfiles/*.lvt against *.tlg
-- l3build ctan -> mypackage-ctan.zip, ready to uploadReleasing to CTAN: what goes in the zip, and the licence
Run l3build ctan and you get a single zip ready to upload. Inside is a directory named after the package, mypackage/, containing the sources (.dtx and .ins) and the built PDF manual. Not shipping the .sty is the convention here; whoever receives it generates one from the .ins. Add a README and a change log and the shape is right. Be warned, though, that l3build ctan sweeps up the PDFs it finds in the working directory, so any stray test prints get packed along with it — list the archive with unzip -l after building and check. The upload itself goes through CTAN’s web form, but TeX Live also ships ctan-o-mat, which validates and uploads from the command line using a configuration file holding your description, contact details and licence.
You do have to settle on a licence. CTAN asks that distribution terms be stated, and the choice shows up in TeX Live’s package information too. The de facto standard in this world is LPPL (the LaTeX Project Public License); how to choose, and how the versions differ — in particular where the “rename it if you change it” clause actually lives — is set out on the licence page. Write the terms you settle on in three places: the meta-comment at the top of the .dtx, the \preamble in the .ins (which lands at the top of every generated file, so it reaches anyone who received only the .sty), and the README. Get that far and your package becomes something a stranger can open with texdoc a few years from now.
- Always write the
\ProvidesPackagedate asYYYY/MM/DD. Without it users cannot ask for a version. - Build the
.dtxevery time. A.stythat still works while the.dtxfails is a real occurrence — see booktabs. - If you start from the template, grep for the template’s name. One
skeletonleftover is still alive in TeX Live 2024. - Check the
l3build ctanzip withunzip -l. It picks up PDFs lying around the working directory. - State the licence in three places: the
.dtx, the.ins\preamble, and the README. It has to reach someone who received only the.sty.