Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

LaTeX & Typst (papers, theses, resumes and slides)  Module 9 – Resumes, CVs and letters

Academic CVs, cover letters and a shared style

Print an academic CV's publication lists from one .bib file in LaTeX and Typst, write a matching cover letter, and share one style file across all three.

  • Intermediate
  • 20 minutes
  • Examples run with TeX Live 2026, TinyTeX 2026.10, Typst 0.15.1, Python 3.14.8, Pyodide 314.0.7 and poppler 26.09.0
  • By MySmartCoPilot
Free lesson, Pro answers: 20 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Generate a CV's publication lists from a .bib file, grouped by entry type and newest first, in LaTeX and in Typst
  • Write a cover letter that shares the resume's look through the same style file or module
  • Build a resume, a CV and a letter from one style file or module, and check that their fonts match
  • Update every publication list by adding one entry to the .bib file, with keywords for a shorter list

Before you start

What you’ll learn · Before you start

What you will learn

  • Generate a CV's publication lists from a .bib file, grouped by entry type and newest first, in LaTeX and in Typst
  • Write a cover letter that shares the resume's look through the same style file or module
  • Build a resume, a CV and a letter from one style file or module, and check that their fonts match
  • Update every publication list by adding one entry to the .bib file, with keywords for a shorter list

Before you start

On this page

An academic CV lists every paper, talk and dataset, and typed by hand those lists drift: a paper appears in the CV but not in the resume, or with a different page range in each. Kavya Iyer, the invented researcher of this lesson, has seven works in one .bib file, and her CV prints four lists from it in LaTeX and in Typst with no reference typed twice.

You will group a .bib file’s works by entry type, newest first, in both systems, fix the one list that Typst’s IEEE style printed without its details, give a resume, a CV and a cover letter one style file, check that their fonts match, and add a paper in one place. Every build ran on TeX Live 2026 with Biber 2.22 and on Typst 0.15.1.

One .bib file and four type filters make the CV’s lists in LaTeX

cv.bib holds Kavya’s two journal articles, two conference papers, a dataset and two talks. A talk is an @unpublished entry, the type that the biblatex manual describes as a work not formally published, such as the script of a talk, and its note keeps the event and the place:

The works of cv.bib, and the CV that lists them

cv.bib

% Kavya Iyer's works, the one source of the publication lists of her CV and resume. Every name, title, venue and DOI
% is invented. Titles are in sentence case, with proper nouns braced; keywords = {selected} marks the works her
% one-page resume lists; talks are @unpublished entries whose note holds the event and the place.
@article{iyer2024monsoon,
  author   = {Iyer, Kavya and Østergaard, Mikkel},
  title    = {Monsoon rainfall records from {Assamese} tea gardens},
  journal  = {Journal of Invented Climate Studies},
  year     = {2024},
  volume   = {12},
  number   = {3},
  pages    = {41--58},
  doi      = {10.5555/jics.2024.0312},
  keywords = {selected},
}

@article{iyer2022soil,
  author   = {Iyer, Kavya and Baruah, Pranjal},
  title    = {Soil moisture after the first monsoon week in {Assam}},
  journal  = {Journal of Invented Climate Studies},
  year     = {2022},
  volume   = {10},
  number   = {2},
  pages    = {77--90},
  doi      = {10.5555/jics.2022.0207},
}

@inproceedings{iyer2025sensors,
  author    = {Iyer, Kavya and Okafor, Chidi},
  title     = {Low-cost rain sensors for tea estates},
  booktitle = {Proceedings of the Sixth Workshop on Invented Data},
  year      = {2025},
  pages     = {33--40},
  doi       = {10.5555/wid.2025.033},
  keywords  = {selected},
}

@inproceedings{iyer2023gaps,
  author    = {Iyer, Kavya},
  title     = {Filling gaps in hourly rain gauge records},
  booktitle = {Proceedings of the Fifth Workshop on Invented Data},
  year      = {2023},
  pages     = {87--94},
  doi       = {10.5555/wid.2023.087},
}

@dataset{rainset2025,
  author   = {Menon, Deepa and Iyer, Kavya and Okafor, Chidi and Rao, Sunita},
  title    = {Hourly rain gauge readings, 2015--2024},
  year     = {2025},
  doi      = {10.5555/rain.hourly.2025},
}

@unpublished{iyer2025talk,
  author   = {Iyer, Kavya},
  title    = {What rain gauges miss},
  note     = {Invited talk, Climate Data Seminar, Example University, Guwahati},
  year     = {2025},
}

@unpublished{iyer2024talk,
  author   = {Iyer, Kavya},
  title    = {Monsoon records from tea gardens},
  note     = {Talk, Example Monsoon Meeting, Shillong},
  year     = {2024},
}

latex/cv.tex

% cv.tex: Kavya Iyer's academic CV. Its publication lists come from cv.bib, grouped by entry type, newest first.
\documentclass[11pt,a4paper]{article}
\usepackage{cv-style}
\usepackage[style=ieee, sorting=ydnt, defernumbers=true]{biblatex}
\addbibresource{cv.bib}

\begin{document}
\cvheader{Kavya Iyer, curriculum vitae}

\cvsection{Positions}
\entry{Example Institute of Atmospheric Science}{Guwahati}{Research fellow}{2024 -- present}

\cvsection{Education}
\entry{Example University}{Guwahati}{PhD in Atmospheric Science}{2019 -- 2024}
\entry{Example University}{Guwahati}{MSc in Physics}{2017 -- 2019}

\nocite{*}
\cvsection{Journal articles}
\printbibliography[type=article, heading=none, resetnumbers=true]
\cvsection{Conference papers}
\printbibliography[type=inproceedings, heading=none, resetnumbers=true]
\cvsection{Datasets}
\printbibliography[type=dataset, heading=none, resetnumbers=true]
\cvsection{Talks}
\printbibliography[type=unpublished, heading=none, resetnumbers=true]
\end{document}

cv.tex asks biblatex for five things. \nocite{*} cites every entry of the file, so all seven reach the bibliography. sorting=ydnt sorts them by year, newest first, then by name and title. Each \printbibliography takes one type, so the articles, the papers, the dataset and the talks print as separate lists. heading=none leaves each heading to \cvsection from the style file, and defernumbers with resetnumbers numbers every list from 1. Follow the entries through those options:

Interactive · stage by stage From cv.bib to the four lists of the CV Follow the entries of cv.bib through biblatex to the Journal articles list, then take the branch to see Typst's way.
From cv.bib to the four lists of the CV, step 6 of 6. Showing \nocite{*}, Biber sorts them, \cvsection{…}, \printbibliography (two articles: 2024, 2022), A list from [1]. In focus: typst.Typst: a list per block\nocite{*}every entry of cv.bibBiber sorts themsorting=ydnt: newest first\cvsection{…}the style's own heading\printbibliographytwo articles: 2024, 2022A list from [1]resetnumbers, defernumbers

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. Every entry is cited. \nocite{*} cites every entry of cv.bib without printing anything, so all seven works reach the bibliography, not only the ones the text cites.
  2. Newest first. Biber sorts the entries with the sorting option of the package, ydnt, which the biblatex manual describes as year descending, then name, then title.
  3. The style's heading. \cvsection{Journal articles} prints the heading with the rule of the shared style, so the publication lists look like every other section.
  4. One type per list. \printbibliography[type=article, heading=none] prints only the articles, and heading=none leaves the heading to \cvsection.
  5. Numbered from 1. With defernumbers=true, an entry gets its number when a list first prints it, and resetnumbers=true makes each list start again at [1], as the biblatex manual describes both.
  6. The Typst way. Typst has no type filter. style.typ, which cv.typ imports, reads the types and years from the text of cv.bib, cites each work of a type with form: none inside a block, and gives that block a bibliography of its own.

cv.tex built with pdfLaTeX and Biber, and the text of the CV Python · latex_cv_session.py
from session import files, run

# cv.tex, the style file it loads and cv.bib: pdfLaTeX, Biber, then pdfLaTeX twice, and the text of the CV.
files("cv.bib", "latex/cv-style.sty", "latex/cv.tex")
run(r"""
pdflatex -interaction=nonstopmode -halt-on-error cv.tex > /dev/null
biber cv > /dev/null
pdflatex -interaction=nonstopmode -halt-on-error cv.tex > /dev/null
pdflatex -interaction=nonstopmode -halt-on-error cv.tex > /dev/null
pdfinfo cv.pdf | grep -E '^(Title|Pages)'
pdftotext cv.pdf - | grep .
""")

Output

$ pdflatex -interaction=nonstopmode -halt-on-error cv.tex > /dev/null
$ biber cv > /dev/null
$ pdflatex -interaction=nonstopmode -halt-on-error cv.tex > /dev/null
$ pdflatex -interaction=nonstopmode -halt-on-error cv.tex > /dev/null
$ pdfinfo cv.pdf | grep -E '^(Title|Pages)'
Title:           Kavya Iyer, curriculum vitae
Pages:           1
$ pdftotext cv.pdf - | grep .
Kavya Iyer
Guwahati, India | kavya.iyer@example.com | kavya.example.com
Positions
Example Institute of Atmospheric Science, Guwahati | 2024 – present
Research fellow
Education
Example University, Guwahati | 2019 – 2024
PhD in Atmospheric Science
Example University, Guwahati | 2017 – 2019
MSc in Physics
Journal articles
[1]
K. Iyer and M. Østergaard, “Monsoon rainfall records from Assamese tea gardens,” Journal of
Invented Climate Studies, vol. 12, no. 3, pp. 41–58, 2024. doi: 10.5555/jics.2024.0312
[2]
K. Iyer and P. Baruah, “Soil moisture after the first monsoon week in Assam,” Journal of Invented
Climate Studies, vol. 10, no. 2, pp. 77–90, 2022. doi: 10.5555/jics.2022.0207
Conference papers
[1]
K. Iyer and C. Okafor, “Low-cost rain sensors for tea estates,” in Proceedings of the Sixth Workshop
on Invented Data, 2025, pp. 33–40. doi: 10.5555/wid.2025.033
[2]
K. Iyer, “Filling gaps in hourly rain gauge records,” in Proceedings of the Fifth Workshop on Invented
Data, 2023, pp. 87–94. doi: 10.5555/wid.2023.087
Datasets
[1]
D. Menon, K. Iyer, C. Okafor, and S. Rao, Hourly rain gauge readings, 2015–2024, 2025. doi:
10.5555/rain.hourly.2025
Talks
[1]
K. Iyer, “What rain gauges miss,” Invited talk, Climate Data Seminar, Example University, Guwahati, 2025.
[2]
K. Iyer, “Monsoon records from tea gardens,” Talk, Example Monsoon Meeting, Shillong, 2024.

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 latex_cv_session.py

The lists came out as planned: the 2024 article before the 2022 one, each list from [1], and the talks with their event and place from the note. The session runs pdfLaTeX twice after Biber because, the biblatex manual says, defernumbers needs two LaTeX runs once Biber has written the .bbl file. Nothing in cv.tex names a work, so the lists follow the file.

One list for everything

Wrong: one \printbibliography with every work, so that a talk sits between two papers. Right: one list per entry type, with type= and resetnumbers=true, under headings of your own, so that a reader finds the journal articles without reading past the talks.

Checkpoint

Check: Publication lists by entry type in biblatex

  1. Question 1 of 2 What does \nocite{*} do in cv.tex?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: It puts every entry of cv.bib into the bibliography without printing a citation in the text

    A CV cites nothing in its text, so \nocite{*} is what brings its works into the bibliography; the filters then decide which list prints each one.

  2. Question 2 of 2 Each list of the CV is numbered from [1]. Which options of cv.tex do that?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: defernumbers=true for the package and resetnumbers=true on each \printbibliography

    The biblatex manual says resetnumbers restarts the numbering at 1 and requires defernumbers, which in turn needs two LaTeX runs after Biber.

Typst prints the same lists, with one bibliography each

Typst’s bibliography has no type filter: full: true would print every work of the file in one list. What Typst 0.15 added, its changelog says, is several bibliographies in one document, each taking the citations its target selects. style.typ builds the lists on that:

The style module that reads cv.bib, the CV that uses it, and a style for the talks

typst/style.typ

// style.typ: the look that Kavya Iyer's resume, CV and letter share, and the parts all three print. Each of them
// starts with #import "style.typ": * and #show: look.with(title: …).
#let me = (name: "Kavya Iyer", place: "Guwahati, India", email: "kavya.iyer@example.com", web: "kavya.example.com")

#let look(title: none, doc) = {
  set document(title: title, author: me.name)
  set page(paper: "a4", margin: 18mm)
  set text(font: "New Computer Modern", size: 11pt, lang: "en", region: "GB")
  set par(spacing: 0.65em)
  set list(spacing: 0.4em, indent: 0.2em)
  show heading.where(level: 1): it => block(above: 1.1em, below: 0.5em, width: 100%,
    stroke: (bottom: 0.5pt), inset: (bottom: 3pt), text(size: 12pt, weight: "bold", it.body))
  doc
}

// The name and the contact line, the same at the top of every document.
#let header() = [
  #title(me.name)
  #me.place | #link("mailto:" + me.email)[#me.email] | #link("https://" + me.web)[#me.web]
]

// One entry: the organisation, the place and the dates on one line, in reading order, and the role under it.
#let entry(org, place, dates, role) = [*#org*, #place | #dates \ #emph(role)]

// The entries of a .bib file as their type, key, year and keywords, read from its text: enough for a tidy file of
// one's own works, whose entries each end with a } alone on a line.
#let works(path) = read(path).matches(regex("(?s)@(\w+)\{([^,\s]+),(.*?)\n\}")).map(m => {
  let (type, key, body) = m.captures
  let year = body.match(regex("\byear\s*=\s*\{(\d{4})\}"))
  let words = body.match(regex("\bkeywords\s*=\s*\{([^}]*)\}"))
  (
    type: type,
    key: key,
    year: if year == none { 0 } else { int(year.captures.first()) },
    keywords: if words == none { () } else { words.captures.first().split(",").map(str.trim) },
  )
})

// A list of works under a heading: every work of the .bib file that `keep` accepts, newest first, cited without a
// mark and printed by a bibliography that takes only the citations inside this block, numbered from 1.
#let publications(path, title, keep, style: "ieee") = {
  heading(title)
  context block({
    for work in works(path).filter(keep).sorted(key: w => -w.year) {
      cite(label(work.key), form: none)
    }
    bibliography(path, title: none, target: selector(cite).within(here()), group: none, style: style)
  })
}

// A cover letter: the header, the date, the recipient's lines, a subject in bold, the text and the signature.
#let letter(to: (), subject: none, body) = {
  header()
  v(0.8em)
  datetime.today().display("[month repr:long] [day padding:none], [year]")
  v(0.4em)
  to.join(linebreak())
  if subject != none {
    v(0.4em)
    strong(subject)
  }
  v(0.4em)
  body
  v(0.8em)
  [With best regards,]
  v(0.4em)
  me.name
}

typst/cv.typ

// cv.typ: Kavya Iyer's academic CV. Its publication lists come from cv.bib, grouped by entry type, newest first.
#import "style.typ": *
#show: look.with(title: [Kavya Iyer, curriculum vitae])
#header()

= Positions
#entry[Example Institute of Atmospheric Science][Guwahati][2024 – present][Research fellow]

= Education
#entry[Example University][Guwahati][2019 – 2024][PhD in Atmospheric Science] \
#entry[Example University][Guwahati][2017 – 2019][MSc in Physics]

#publications("cv.bib", [Journal articles], w => w.type == "article")
#publications("cv.bib", [Conference papers], w => w.type == "inproceedings")
#publications("cv.bib", [Datasets], w => w.type == "dataset")
#publications("cv.bib", [Talks], w => w.type == "unpublished", style: "talks.csl")

typst/talks.csl

<?xml version="1.0" encoding="utf-8"?>
<!-- talks.csl: a list numbered like IEEE that also prints a talk's note, where cv.bib keeps its event and place. -->
<style xmlns="http://purl.org/net/xbiblio/csl" class="in-text" version="1.0" default-locale="en-GB">
  <info>
    <title>CV talks</title>
    <id>https://example.com/csl/cv-talks</id>
    <updated>2026-01-01T00:00:00+00:00</updated>
  </info>
  <citation>
    <layout>
      <text variable="citation-number" prefix="[" suffix="]"/>
    </layout>
  </citation>
  <bibliography second-field-align="flush">
    <layout suffix=".">
      <text variable="citation-number" prefix="[" suffix="]"/>
      <group delimiter=", ">
        <names variable="author">
          <name initialize-with=". " and="text"/>
        </names>
        <text variable="title" quotes="true"/>
        <text variable="note"/>
        <date variable="issued">
          <date-part name="year"/>
        </date>
      </group>
    </layout>
  </bibliography>
</style>

works reads cv.bib as text with read and a regular expression, and returns each entry’s type, key, year and keywords: enough for a tidy file of one’s own works. publications then takes the works a test accepts, such as w => w.type == "article", sorts them newest first, cites each with form: none, which prints nothing, inside a block, and gives that block a bibliography whose target is selector(cite).within(here()). group: none numbers each list from 1, as resetnumbers did in LaTeX.

The works as style.typ reads them, the talks in IEEE, and the CV Python · typst_cv_session.py
from session import files, read, run, write

# The works of cv.bib as style.typ's reader sees them; a copy of cv.typ whose talks use IEEE like the other lists, and
# its talks; then cv.typ itself, with talks.csl, and the text of the CV.
files("cv.bib", "typst/style.typ", "typst/cv.typ", "typst/talks.csl")
write("cv-ieee.typ", read("cv.typ").replace(', style: "talks.csl"', ""))
run(r"""
typst eval '{ import "style.typ": works; works("cv.bib").map(w => (w.type, w.key, w.year)) }'
typst compile --pdf-standard ua-1 cv-ieee.typ
pdftotext cv-ieee.pdf - | grep -A 3 Talks
typst compile --pdf-standard ua-1 cv.typ
pdfinfo cv.pdf | grep -E '^(Title|Tagged|Pages)'
pdftotext cv.pdf - | grep .
""")

Output

$ typst eval '{ import "style.typ": works; works("cv.bib").map(w => (w.type, w.key, w.year)) }'
[["article","iyer2024monsoon",2024],["article","iyer2022soil",2022],["inproceedings","iyer2025sensors",2025],["inproceedings","iyer2023gaps",2023],["dataset","rainset2025",2025],["unpublished","iyer2025talk",2025],["unpublished","iyer2024talk",2024]]
$ typst compile --pdf-standard ua-1 cv-ieee.typ
$ pdftotext cv-ieee.pdf - | grep -A 3 Talks
Talks
[1] K. Iyer, ‘What rain gauges miss’, 2025.
[2] K. Iyer, ‘Monsoon records from tea gardens’, 2024.

$ typst compile --pdf-standard ua-1 cv.typ
$ pdfinfo cv.pdf | grep -E '^(Title|Tagged|Pages)'
Title:           Kavya Iyer, curriculum vitae
Tagged:          yes
Pages:           1
$ pdftotext cv.pdf - | grep .
Kavya Iyer
Guwahati, India | kavya.iyer@example.com | kavya.example.com
Positions
Example Institute of Atmospheric Science, Guwahati | 2024 – present
Research fellow
Education
Example University, Guwahati | 2019 – 2024
PhD in Atmospheric Science
Example University, Guwahati | 2017 – 2019
MSc in Physics
Journal articles
[1] K. Iyer and M. Østergaard, ‘Monsoon rainfall records from Assamese tea gardens’, Journal of
Invented Climate Studies, vol. 12, no. 3, pp. 41–58, 2024, doi: 10.5555/jics.2024.0312.
[2] K. Iyer and P. Baruah, ‘Soil moisture after the first monsoon week in Assam’, Journal of Invented
Climate Studies, vol. 10, no. 2, pp. 77–90, 2022, doi: 10.5555/jics.2022.0207.
Conference papers
[1] K. Iyer and C. Okafor, ‘Low-cost rain sensors for tea estates’, in Proceedings of the Sixth
Workshop on Invented Data, 2025, pp. 33–40. doi: 10.5555/wid.2025.033.
[2] K. Iyer, ‘Filling gaps in hourly rain gauge records’, in Proceedings of the Fifth Workshop on
Invented Data, 2023, pp. 87–94. doi: 10.5555/wid.2023.087.
Datasets
[1] D. Menon, K. Iyer, C. Okafor, and S. Rao, Hourly rain gauge readings, 2015–2024. (2025). doi:
10.5555/rain.hourly.2025.
Talks
[1] K. Iyer, ‘What rain gauges miss’, Invited talk, Climate Data Seminar, Example University,
Guwahati, 2025.
[2] K. Iyer, ‘Monsoon records from tea gardens’, Talk, Example Monsoon Meeting, Shillong, 2024.

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 typst_cv_session.py

The first build used IEEE for every list, and its talks lost their notes: K. Iyer, ‘What rain gauges miss’, 2025. Typst’s IEEE style did not print the note for these entries. talks.csl, 29 lines of the Citation Style Language, is numbered like IEEE and adds the note variable, and with it the CV’s talks read Invited talk, Climate Data Seminar, Example University, Guwahati, 2025 as biblatex’s did.

The talk of 2025 as printed
biblatex with biblatex-ieee K. Iyer, “What rain gauges miss,” Invited talk, Climate Data Seminar, …, 2025.
Typst with its IEEE style K. Iyer, ‘What rain gauges miss’, 2025.
Typst with talks.csl K. Iyer, ‘What rain gauges miss’, Invited talk, Climate Data Seminar, …, 2025.

The quotation marks differ too. With region: "GB" in the style, Typst’s lists used single quotes with the comma outside them, the British way, while biblatex-ieee kept double quotes; the citations module’s lesson on Typst bibliographies compares the two systems’ IEEE output in detail.

Proof-read every list a style prints

A style prints the fields it knows for each type and quietly leaves out the rest. Read each list of a new CV once against its .bib entries, as the talks show.

Checkpoint

Check: Publication lists by entry type in Typst

  1. Question 1 of 2 Why does style.typ cite each work with cite(label(work.key), form: none) inside the block of a list?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: The bibliography of the block takes only the citations inside it, and form none cites a work without printing anything

    Each list is a block with its own citations and a bibliography that targets them, and group none numbers each list from 1, as resetnumbers did in LaTeX.

  2. Question 2 of 2 In the first Typst build, every list used IEEE, and the talks printed only their author, title and year. What fixed them?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: A small CSL file, talks.csl, that prints the note where cv.bib keeps the event and the place

    A style prints the fields it knows for each type and leaves out the rest without a warning, so a small CSL file of your own can print what a built-in style does not.

One style file gives the resume, the CV and the letter one look

Kavya sends three documents: a one-page resume for a data role, the CV and a cover letter. If each sets its own fonts, margins and headings, they drift apart with every edit. Here none of them sets any. In Typst they start with #import "style.typ": * and #show: look.with(title: […]); in LaTeX they load cv-style.sty with \usepackage{cv-style}, a package: A file of extra LaTeX commands, usually ending in .sty, that a document loads with \usepackage. CTAN collects packages, and a TeX distribution installs them. More in “LaTeX and Typst: what they are and when to use each” of the documents’ own. Each shared file holds the look and the parts every document prints:

The LaTeX style file, and the resume in each system

latex/cv-style.sty

% cv-style.sty: the look that Kavya Iyer's resume, CV and letter share. Each of them loads it with \usepackage{cv-style}.
\NeedsTeXFormat{LaTeX2e}
\ProvidesPackage{cv-style}
\RequirePackage[margin=18mm]{geometry}
\RequirePackage[T1]{fontenc}
\RequirePackage{lmodern}
\RequirePackage{enumitem}
\RequirePackage[hidelinks]{hyperref}

\pagestyle{empty}
\setlength{\parindent}{0pt}
\setlist[itemize]{nosep, leftmargin=1.2em}

% Headings: LaTeX's \section*, set close, with a rule under them (as in the module's one-page resume).
\renewcommand\section{\@startsection{section}{1}{0pt}{8pt}{1sp}{\large\bfseries}}
\newcommand{\cvsection}[1]{\section*{#1}\vspace{2pt}\hrule\vspace{3pt}}

% One entry: the organisation, the place and the dates on one line, in reading order, and the role under it.
\newcommand{\entry}[4]{\textbf{#1}, #2 | #4\par\textit{#3}\par}

% The name and the contact line, the same at the top of every document. #1 is the PDF's title.
\newcommand{\cvheader}[1]{%
  \hypersetup{pdftitle={#1}, pdfauthor={Kavya Iyer}}%
  {\LARGE\bfseries Kavya Iyer}\par\smallskip
  Guwahati, India | \href{mailto:kavya.iyer@example.com}{kavya.iyer@example.com}
  | \href{https://kavya.example.com}{kavya.example.com}\par}

typst/resume.typ

// resume.typ: Kavya Iyer's one-page resume for a data role. Its publications are the works cv.bib marks selected.
#import "style.typ": *
#show: look.with(title: [Kavya Iyer, resume])
#header()

= Experience
#entry[Example Institute of Atmospheric Science][Guwahati][2024 – present][Research fellow]
- Built the pipeline that cleans 4.2 million hourly gauge readings a year for the institute’s rainfall maps.
- Wrote the Python package the lab uses to fill gaps in gauge records, with 96 tests.

= Education
#entry[Example University][Guwahati][2019 – 2024][PhD in Atmospheric Science]

#publications("cv.bib", [Selected publications], w => "selected" in w.keywords)

= Skills
Python, R, SQL, pandas, xarray, Git, LaTeX, Typst

latex/resume.tex

% resume.tex: Kavya Iyer's one-page resume for a data role. Its publications are the works cv.bib marks selected.
\documentclass[11pt,a4paper]{article}
\usepackage{cv-style}
\usepackage[style=ieee, sorting=ydnt, defernumbers=true]{biblatex}
\addbibresource{cv.bib}

\begin{document}
\cvheader{Kavya Iyer, resume}

\cvsection{Experience}
\entry{Example Institute of Atmospheric Science}{Guwahati}{Research fellow}{2024 -- present}
\begin{itemize}
  \item Built the pipeline that cleans 4.2 million hourly gauge readings a year for the institute's rainfall maps.
  \item Wrote the Python package the lab uses to fill gaps in gauge records, with 96 tests.
\end{itemize}

\cvsection{Education}
\entry{Example University}{Guwahati}{PhD in Atmospheric Science}{2019 -- 2024}

\nocite{*}
\cvsection{Selected publications}
\printbibliography[keyword=selected, heading=none]

\cvsection{Skills}
Python, R, SQL, pandas, xarray, Git, LaTeX, Typst
\end{document}

Step through what the Typst module: In Typst, a .typ file whose definitions other files take with #import "style.typ": name, so that several documents share one set of functions and rules. gives each document:

Interactive · step by step Three documents, one style module and one .bib file Step through what each shared file gives Kavya's resume, CV and letter, and what one change to it changes.
Three documents, one style module and one .bib file, step 8 of 8. Showing cv.bib, style.typ, talks.csl, resume.typ, cv.typ, letter.typ. In focus: style.typ.cv.bibseven works, two selectedstyle.typlook, header, entry, lettertalks.cslkeeps the talks' notesresume.typone page, selected workscv.typfour lists from cv.bibletter.typletter(to: …)[…]

The drawing shows the whole scene; its 8 steps are listed below.

Every step as text
  1. Three documents. Kavya's resume, CV and cover letter are three short files. None of them sets a font, a margin or a heading style of its own.
  2. The look. Each one starts with #import "style.typ": * and #show: look.with(title: …). look sets the page, New Computer Modern at 11 pt, the lists and the heading rule, and writes the title and author.
  3. The parts they print. header() prints the name and the contact line, and entry() the same entry layout as the resume of this module's first lesson, so the top of every page is the same.
  4. The letter. letter() adds what a letter needs, the date, the recipient's lines, the subject and the signature, around the same header and type.
  5. The works. cv.bib is read by the CV for its four lists and by the resume for the works marked selected. A new entry reaches every list that should show it.
  6. The talks. talks.csl is a small style for the CV's talks, because Typst's IEEE style printed them without the event and the place kept in their note.
  7. One change, three documents. A new font or margin is one line of style.typ, and all three PDFs follow it. pdffonts found only New Computer Modern fonts in all three.
  8. The same in LaTeX. cv-style.sty plays the same part for the LaTeX documents, with \cvheader, \cvsection and \entry, and each of them loads it with \usepackage{cv-style}.

A matching look can be checked. pdffonts lists the fonts a PDF embeds, each behind a subset prefix: The six capital letters and plus sign before a font's name in a PDF, as in DSRJEH+NewCM10-Regular, which mark a font embedded with only the glyphs the document uses. They differ from file to file. that differs from file to file, so the sessions strip the prefix before comparing:

The LaTeX resume, CV and letter, and their fonts Python · family_latex_session.py
from session import files, run

# Kavya's LaTeX resume, CV and letter, built with the style file they share, and the fonts each PDF uses, with the
# six-letter prefix of an embedded subset removed. family_typst_session.py does the same for the Typst documents.
files("cv.bib", "latex/cv-style.sty", "latex/resume.tex", "latex/cv.tex", "latex/letter.tex")
run(r"""
for f in resume cv; do pdflatex -interaction=nonstopmode -halt-on-error $f.tex > /dev/null && biber -q $f > /dev/null && pdflatex -interaction=nonstopmode -halt-on-error $f.tex > /dev/null && pdflatex -interaction=nonstopmode -halt-on-error $f.tex > /dev/null; done
pdflatex -interaction=nonstopmode -halt-on-error letter.tex > /dev/null
for f in resume cv letter; do echo "$f.pdf: $(pdffonts $f.pdf | tail -n +3 | awk '{print $1}' | sed 's/^[A-Z]\{6\}+//' | sort -u | tr '\n' ' ')"; done
""")

Output

$ for f in resume cv; do pdflatex -interaction=nonstopmode -halt-on-error $f.tex > /dev/null && biber -q $f > /dev/null && pdflatex -interaction=nonstopmode -halt-on-error $f.tex > /dev/null && pdflatex -interaction=nonstopmode -halt-on-error $f.tex > /dev/null; done
$ pdflatex -interaction=nonstopmode -halt-on-error letter.tex > /dev/null
$ for f in resume cv letter; do echo "$f.pdf: $(pdffonts $f.pdf | tail -n +3 | awk '{print $1}' | sed 's/^[A-Z]\{6\}+//' | sort -u | tr '\n' ' ')"; done
resume.pdf: LMMono10-Regular LMRoman10-Bold LMRoman10-Italic LMRoman10-Regular LMRoman12-Bold LMRomanCaps10-Regular 
cv.pdf: LMMono10-Regular LMRoman10-Bold LMRoman10-Italic LMRoman10-Regular LMRoman12-Bold LMRomanCaps10-Regular 
letter.pdf: LMRoman10-Bold LMRoman10-Regular LMRoman12-Bold 

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 family_latex_session.py

The Typst resume, CV and letter, their fonts, and a letter without the look Python · family_typst_session.py
from session import files, read, run, write

# Kavya's Typst resume, CV and letter, built with the style module they share, and the fonts each PDF uses, with the
# six-letter prefix of an embedded subset removed. Last, the letter once more with the import kept and the line that
# applies the look left out.
files("cv.bib", "typst/style.typ", "typst/resume.typ", "typst/cv.typ", "typst/letter.typ", "typst/talks.csl")
write("letter-plain.typ", read("letter.typ").replace("#show: look.with(title: [Kavya Iyer, cover letter])\n", ""))
run(r"""
for f in resume cv letter letter-plain; do typst compile $f.typ; done
for f in resume cv letter letter-plain; do echo "$f.pdf: $(pdffonts $f.pdf | tail -n +3 | awk '{print $1}' | sed 's/^[A-Z]\{6\}+//' | sort -u | tr '\n' ' ')"; done
""")

Output

$ for f in resume cv letter letter-plain; do typst compile $f.typ; done
$ for f in resume cv letter letter-plain; do echo "$f.pdf: $(pdffonts $f.pdf | tail -n +3 | awk '{print $1}' | sed 's/^[A-Z]\{6\}+//' | sort -u | tr '\n' ' ')"; done
resume.pdf: NewCM10-Bold-Identity-H NewCM10-Italic-Identity-H NewCM10-Regular-Identity-H 
cv.pdf: NewCM10-Bold-Identity-H NewCM10-Italic-Identity-H NewCM10-Regular-Identity-H 
letter.pdf: NewCM10-Bold-Identity-H NewCM10-Regular-Identity-H 
letter-plain.pdf: LibertinusSerif-Bold-Identity-H LibertinusSerif-Regular-Identity-H 

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 family_typst_session.py

In LaTeX the resume and the CV used the same six Latin Modern fonts and the letter three of them; in Typst all three used New Computer Modern. The last Typst file shows the slip this check catches. letter-plain.typ kept the import, so letter and header worked, but left out the #show: look.with(…) line, and its PDF came out in Libertinus Serif, Typst’s default font: a letter that no longer matched the resume, without any error.

Key idea

Put the look in one style file or module and nothing of it in the documents, then compare the fonts of the PDFs with their subset prefixes removed: a document that forgot the style shows up at once.

Checkpoint

Check: One style file for three documents

  1. Question 1 of 2 letter-plain.typ kept #import "style.typ": * but left out #show: look.with(…). What did its PDF show?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: It compiled without an error, in Libertinus Serif, Typst's default font, instead of the style's New Computer Modern

    A forgotten show rule is a silent mistake, which the font check of the three PDFs finds at once.

  2. Question 2 of 2 Why do the sessions remove the six capital letters and the plus sign before each font name that pdffonts prints?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: The prefix marks an embedded subset and differs from file to file, so the same font would not match across PDFs

    Compare font names without their subset prefixes, as the lesson's second lab step does.

The cover letter is a function of the same style

A letter has parts a resume lacks: a date, the recipient’s lines, a subject and a signature. In Typst they are a function of style.typ, letter(to: …, subject: …)[…], that wraps the body in the same header and type, so letter.typ holds only what this letter says:

The cover letter in Typst and in LaTeX

typst/letter.typ

// letter.typ: Kavya Iyer's cover letter, built with the letter function of the style module her resume and CV use.
#import "style.typ": *
#show: look.with(title: [Kavya Iyer, cover letter])

#letter(
  to: ([Hiring committee], [Example Climate Analytics], [Bengaluru]),
  subject: [Research scientist, rainfall data],
)[
  Dear members of the committee,

  I am applying for the post of research scientist in your rainfall data team. For five years I have worked with
  hourly rain gauge records from tea estates in Assam: cleaning them, filling their gaps and turning them into the
  maps my institute publishes every season.

  At the Example Institute of Atmospheric Science I built the pipeline that cleans 4.2 million readings a year, and
  the method behind it is in my paper on filling gaps in hourly records. I would bring the same care for messy field
  data to your forecasting products.

  Thank you for your time. I would be glad to talk about the post.
]

latex/letter.tex

% letter.tex: Kavya Iyer's cover letter, on the article class with the same style file as her resume and CV.
\documentclass[11pt,a4paper]{article}
\usepackage{cv-style}

\begin{document}
\cvheader{Kavya Iyer, cover letter}
\bigskip
\today

\medskip
Hiring committee\\
Example Climate Analytics\\
Bengaluru

\medskip
\textbf{Research scientist, rainfall data}

\medskip
Dear members of the committee,

I am applying for the post of research scientist in your rainfall data team. For five years I have worked with hourly
rain gauge records from tea estates in Assam: cleaning them, filling their gaps and turning them into the maps my
institute publishes every season.

At the Example Institute of Atmospheric Science I built the pipeline that cleans 4.2 million readings a year, and the
method behind it is in my paper on filling gaps in hourly records. I would bring the same care for messy field data to
your forecasting products.

Thank you for your time. I would be glad to talk about the post.

\bigskip
With best regards,\par
\medskip
Kavya Iyer
\end{document}
Both letters built, the start of their text, and the letter class Python · letter_session.py
from session import files, read, run, write

# The cover letter in both systems: letter.tex with the style file and letter.typ with the style module, each built
# once, then each PDF's title and the start of its text. Last, a copy of letter.tex on LaTeX's letter class instead of
# the article class, and the first error in its log.
files("latex/cv-style.sty", "latex/letter.tex", "typst/style.typ", "typst/letter.typ")
ARTICLE = "\\documentclass[11pt,a4paper]{article}\n"
text = read("letter.tex")
assert ARTICLE in text
write("letter-class.tex", text.replace(ARTICLE, "\\documentclass[11pt,a4paper]{letter}\n"))
run(r"""
pdflatex -interaction=nonstopmode -halt-on-error letter.tex > /dev/null
typst compile letter.typ letter-typst.pdf
pdfinfo letter.pdf | grep -E '^(Title|Pages)'
pdftotext letter.pdf - | grep . | head -n 9
pdfinfo letter-typst.pdf | grep -E '^(Title|Pages)'
pdftotext letter-typst.pdf - | grep . | head -n 9
!pdflatex -interaction=nonstopmode -halt-on-error letter-class.tex > /dev/null
grep -a -m 1 '^!' letter-class.log
""")

Output

$ pdflatex -interaction=nonstopmode -halt-on-error letter.tex > /dev/null
$ typst compile letter.typ letter-typst.pdf
$ pdfinfo letter.pdf | grep -E '^(Title|Pages)'
Title:           Kavya Iyer, cover letter
Pages:           1
$ pdftotext letter.pdf - | grep . | head -n 9
Kavya Iyer
Guwahati, India | kavya.iyer@example.com | kavya.example.com
January 1, 2026
Hiring committee
Example Climate Analytics
Bengaluru
Research scientist, rainfall data
Dear members of the committee,
I am applying for the post of research scientist in your rainfall data team. For five years I have worked
$ pdfinfo letter-typst.pdf | grep -E '^(Title|Pages)'
Title:           Kavya Iyer, cover letter
Pages:           1
$ pdftotext letter-typst.pdf - | grep . | head -n 9
Kavya Iyer
Guwahati, India | kavya.iyer@example.com | kavya.example.com
January 1, 2026
Hiring committee
Example Climate Analytics
Bengaluru
Research scientist, rainfall data
Dear members of the committee,
I am applying for the post of research scientist in your rainfall data team. For five years I have
$ pdflatex -interaction=nonstopmode -halt-on-error letter-class.tex > /dev/null
[exit status 1]
$ grep -a -m 1 '^!' letter-class.log
! LaTeX Error: Command \section undefined.

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 letter_session.py

The two letters begin the same, word for word, under the same header as the resume and the CV; only one line break of the first paragraph falls a word apart. The date comes from datetime.today() in Typst and \today in LaTeX, and both sessions fix it through SOURCE_DATE_EPOCH, so the two letters printed the same date. LaTeX has a standard letter class, and the tagging status list marks it partially compatible, with tagging that is not ideal. letter.tex uses the article class with cv-style.sty instead, and the last build of the session shows why: on the letter class the build stopped with Command \section undefined, because the letter class defines no \section for cv-style.sty to redefine.

Write the letter's own words only

Keep the letter’s file to what changes from one application to the next: the recipient, the subject and three short paragraphs. Everything that should look the same comes from the style, like the resume’s.

Checkpoint

Check: A cover letter from the same style

  1. One question Why does letter.tex use the article class with cv-style.sty rather than LaTeX's letter class?

    Choose one answer.

    Show the answer: One question

    Answer: The letter class defines no \section, which cv-style.sty redefines for its headings, so the shared style would not load on it

    A shared style must load on every document that shares it, so the letter uses the same class as the others and adds its own few lines.

A new paper is one entry, and every list that should show it does

A CV is kept current by adding to the data, never to a list. update_session.py adds one article of 2026 to the end of cv.bib, marked with keywords = {selected}, and builds both CVs and both resumes again:

A new article added to cv.bib, then both CVs and both resumes built again Python · update_session.py
from session import files, read, run, write

# One new article, marked selected, added to the end of cv.bib, then both CVs and both resumes built again: LaTeX's into
# cv.pdf and resume.pdf, Typst's into cv-typst.pdf and resume-typst.pdf. Each CV lists the new article first among the
# journal articles, because both systems sort the newest first, and each resume lists it first among the selected works.
files("cv.bib", "latex/cv-style.sty", "latex/cv.tex", "latex/resume.tex", "typst/style.typ", "typst/cv.typ",
      "typst/resume.typ", "typst/talks.csl")
NEW = """
@article{iyer2026dry,
  author   = {Iyer, Kavya and Baruah, Pranjal},
  title    = {Dry spells in the {Brahmaputra} valley, 1990--2025},
  journal  = {Journal of Invented Climate Studies},
  year     = {2026},
  volume   = {14},
  number   = {1},
  pages    = {5--19},
  doi      = {10.5555/jics.2026.0105},
  keywords = {selected},
}
"""
write("cv.bib", read("cv.bib") + NEW)
run(r"""
tail -n 12 cv.bib
pdflatex -interaction=nonstopmode -halt-on-error cv.tex > /dev/null
biber cv > /dev/null
pdflatex -interaction=nonstopmode -halt-on-error cv.tex > /dev/null
pdflatex -interaction=nonstopmode -halt-on-error cv.tex > /dev/null
typst compile --pdf-standard ua-1 cv.typ cv-typst.pdf
pdftotext cv.pdf - | grep -A 6 'Journal articles'
pdftotext cv-typst.pdf - | grep -A 6 'Journal articles'
pdflatex -interaction=nonstopmode -halt-on-error resume.tex > /dev/null && biber -q resume > /dev/null && pdflatex -interaction=nonstopmode -halt-on-error resume.tex > /dev/null && pdflatex -interaction=nonstopmode -halt-on-error resume.tex > /dev/null
typst compile --pdf-standard ua-1 resume.typ resume-typst.pdf
pdftotext resume.pdf - | grep . | grep -A 6 'Selected publications'
pdftotext resume-typst.pdf - | grep -A 4 'Selected publications'
""")

Output

$ tail -n 12 cv.bib

@article{iyer2026dry,
  author   = {Iyer, Kavya and Baruah, Pranjal},
  title    = {Dry spells in the {Brahmaputra} valley, 1990--2025},
  journal  = {Journal of Invented Climate Studies},
  year     = {2026},
  volume   = {14},
  number   = {1},
  pages    = {5--19},
  doi      = {10.5555/jics.2026.0105},
  keywords = {selected},
}
$ pdflatex -interaction=nonstopmode -halt-on-error cv.tex > /dev/null
$ biber cv > /dev/null
$ pdflatex -interaction=nonstopmode -halt-on-error cv.tex > /dev/null
$ pdflatex -interaction=nonstopmode -halt-on-error cv.tex > /dev/null
$ typst compile --pdf-standard ua-1 cv.typ cv-typst.pdf
$ pdftotext cv.pdf - | grep -A 6 'Journal articles'
Journal articles
[1]

K. Iyer and P. Baruah, “Dry spells in the Brahmaputra valley, 1990–2025,” Journal of Invented
Climate Studies, vol. 14, no. 1, pp. 5–19, 2026. doi: 10.5555/jics.2026.0105

[2]
$ pdftotext cv-typst.pdf - | grep -A 6 'Journal articles'
Journal articles
[1] K. Iyer and P. Baruah, ‘Dry spells in the Brahmaputra valley, 1990–2025’, Journal of Invented
Climate Studies, vol. 14, no. 1, pp. 5–19, 2026, doi: 10.5555/jics.2026.0105.
[2] K. Iyer and M. Østergaard, ‘Monsoon rainfall records from Assamese tea gardens’, Journal of
Invented Climate Studies, vol. 12, no. 3, pp. 41–58, 2024, doi: 10.5555/jics.2024.0312.
[3] K. Iyer and P. Baruah, ‘Soil moisture after the first monsoon week in Assam’, Journal of Invented
Climate Studies, vol. 10, no. 2, pp. 77–90, 2022, doi: 10.5555/jics.2022.0207.
$ pdflatex -interaction=nonstopmode -halt-on-error resume.tex > /dev/null && biber -q resume > /dev/null && pdflatex -interaction=nonstopmode -halt-on-error resume.tex > /dev/null && pdflatex -interaction=nonstopmode -halt-on-error resume.tex > /dev/null
$ typst compile --pdf-standard ua-1 resume.typ resume-typst.pdf
$ pdftotext resume.pdf - | grep . | grep -A 6 'Selected publications'
Selected publications
[1]
K. Iyer and P. Baruah, “Dry spells in the Brahmaputra valley, 1990–2025,” Journal of Invented
Climate Studies, vol. 14, no. 1, pp. 5–19, 2026. doi: 10.5555/jics.2026.0105
[2]
K. Iyer and C. Okafor, “Low-cost rain sensors for tea estates,” in Proceedings of the Sixth Workshop
on Invented Data, 2025, pp. 33–40. doi: 10.5555/wid.2025.033
$ pdftotext resume-typst.pdf - | grep -A 4 'Selected publications'
Selected publications
[1] K. Iyer and P. Baruah, ‘Dry spells in the Brahmaputra valley, 1990–2025’, Journal of Invented
Climate Studies, vol. 14, no. 1, pp. 5–19, 2026, doi: 10.5555/jics.2026.0105.
[2] K. Iyer and C. Okafor, ‘Low-cost rain sensors for tea estates’, in Proceedings of the Sixth
Workshop on Invented Data, 2025, pp. 33–40. doi: 10.5555/wid.2025.033.

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 update_session.py

Both CVs put the new article first among the journal articles, as [1], without any change to cv.tex or cv.typ: the sort is by year, not by place in the file. The session builds in a clean folder, and that matters for LaTeX: the biblatex manual warns that with defernumbers a change to the .bib file usually needs the old .aux file deleted before the next build.

The resumes choose by that mark. Two entries of cv.bib already carried keywords = {selected}, a keywords field: A .bib field of comma-separated words, such as keywords = {selected}, that biblatex's keyword and notkeyword filters read to choose the entries of a list., and each resume prints only the works that carry it, with keyword=selected in LaTeX and w => "selected" in w.keywords in Typst. The new article carries it too, so both resumes now list it first, as [1], ahead of the 2025 conference paper.

Key idea

Keep every work once, in the .bib file, with its type and its keywords, and let the lists choose: a new entry then reaches the CV and, when it is marked, the resume, in both systems.

Checkpoint

Check: Keeping every list current

  1. Question 1 of 2 An article of 2026 was added at the end of cv.bib. Where did it appear in both CVs?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: First among the journal articles, as [1]

    Adding to the data, not to a list, keeps every list in step: one entry reached both CVs, sorted into place.

  2. Question 2 of 2 Which lines make the resumes list only the works marked selected? Choose every one.

    Choose every answer that is right.

    Show the answer: Question 2 of 2

    Answer:

    • w => "selected" in w.keywords in resume.typ
    • \printbibliography[keyword=selected, heading=none] in resume.tex
    • keywords = {selected} in the entries of cv.bib

    A keyword in the data and a filter in each document: marking a new paper selected adds it to both resumes.

Practise: check a CV’s sources before you build it

The two steps are the checks of this lesson as code: the first groups a .bib file by type, newest first, as style.typ does, the second reads pdffonts output without its subset prefixes. Their sample tests use this lesson’s files and outputs, and the first hint of each step is open:

Code lab · 2 steps

Check a CV's works and fonts

Group the works of a .bib file by entry type, newest first, then list the fonts of a PDF without their subset prefixes.

  1. Group a CV's .bib file by entry type, newest first To do
  2. Read the fonts of a PDF without their subset prefixes To do

Exercise · Medium · Python

Group a CV's .bib file by entry type, newest first

style.typ reads cv.bib as text to find each entry's type, key and year, then prints one list per type, newest first. A script that checks a CV before it is built needs the same grouping.

Write works_by_type(bib) in works.py. Given the text of a .bib file, it returns a dictionary that maps each entry type, in lower case, to the list of its keys, newest first. Entries of the same year keep the order they have in the file, and an entry without a year field counts as year 0, after all the others.

Read an entry the way style.typ does: it starts with @type{key, and ends at a } alone at the start of a line. Inside it, year = {2024} gives the year; a field may have more spaces around =, and the entry type may be written @Article as well as @article. Lines that start with % outside the entries are comments.

Starter code · works.py

import re


def works_by_type(bib):
    """Return {entry type: [keys, newest first]} for the entries of a .bib file's text."""
    # Replace this line: find each entry's type, key and year, then group the keys by type and sort them.
    return {}
The sample tests · test_works.py
from works import works_by_type

CV = """% Kavya Iyer's works.
@article{iyer2022soil,
  title    = {Soil moisture after the first monsoon week in {Assam}},
  year     = {2022},
}

@article{iyer2024monsoon,
  title    = {Monsoon rainfall records from {Assamese} tea gardens},
  year     = {2024},
  keywords = {selected},
}

@inproceedings{iyer2025sensors,
  title     = {Low-cost rain sensors for tea estates},
  year      = {2025},
}

@unpublished{iyer2024talk,
  title    = {Monsoon records from tea gardens},
  note     = {Talk, Example Monsoon Meeting, Shillong},
  year     = {2024},
}
"""


def test_groups_and_order():
    """groups cv.bib's entries by type, newest first"""
    assert works_by_type(CV) == {
        "article": ["iyer2024monsoon", "iyer2022soil"],
        "inproceedings": ["iyer2025sensors"],
        "unpublished": ["iyer2024talk"],
    }


def test_same_year_keeps_file_order():
    """keeps the file's order for two works of the same year"""
    bib = "@misc{b,\n  year = {2023},\n}\n@misc{a,\n  year = {2023},\n}\n"
    assert works_by_type(bib) == {"misc": ["b", "a"]}


def test_upper_case_type_and_spaces():
    """reads @Article and a year written with spaces around ="""
    bib = "@Article{rao2021,\n  year={2021},\n}\n@article{rao2025,\n  year  =  {2025},\n}\n"
    assert works_by_type(bib) == {"article": ["rao2025", "rao2021"]}


def test_no_year_last():
    """puts a work without a year after the dated ones"""
    bib = "@misc{undated,\n  title = {Notes},\n}\n@misc{dated,\n  year = {2020},\n}\n"
    assert works_by_type(bib) == {"misc": ["dated", "undated"]}
A hint

re.finditer(r"@(\w+)\{([^,\s]+),(.*?)\n\}", bib, re.S) gives each entry's type, key and body; search the body for r"\byear\s*=\s*\{(\d{4})\}". Collect (year, key) pairs per type, then sort each list by the year with reverse=True: Python's sort keeps entries of the same year in the order of the file.

The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

Exercise · Easy · Python

Read the fonts of a PDF without their subset prefixes

The lesson compared the fonts of Kavya's resume, CV and letter with pdffonts. Its names begin with a prefix such as DSRJEH+, six capital letters and a plus sign that mark a font embedded as a subset, and the prefix differs from file to file, so the names only match once it is removed.

Write font_names(output) in fonts.py. Given the text that pdffonts printed for one PDF, it returns the sorted list of the different font names in it, each without its prefix. Skip the two header lines (the column names and the line of dashes); the name is the first word of every other line. A name without a prefix is kept as it is, and a prefix is only six capital letters followed by +, so ABC+Font keeps its ABC+.

Starter code · fonts.py

def font_names(output):
    """Return the sorted, different font names of pdffonts output, without their subset prefixes."""
    # Replace this line: skip the two header lines, take the first word of each line and strip a six-letter prefix.
    return []
The sample tests · test_fonts.py
from fonts import font_names

HEADER = (
    "name                                 type              encoding         emb sub uni object ID\n"
    "------------------------------------ ----------------- ---------------- --- --- --- ---------\n"
)
CV = HEADER + (
    "DSRJEH+NewCM10-Bold-Identity-H       CID Type 0C       Identity-H       yes yes yes    105  0\n"
    "MDYYKN+NewCM10-Regular-Identity-H    CID Type 0C       Identity-H       yes yes yes    108  0\n"
    "NWSSOI+NewCM10-Italic-Identity-H     CID Type 0C       Identity-H       yes yes yes    111  0\n"
)
LETTER = HEADER + (
    "QWERTY+NewCM10-Regular-Identity-H    CID Type 0C       Identity-H       yes yes yes     40  0\n"
    "ZXCVBN+NewCM10-Bold-Identity-H       CID Type 0C       Identity-H       yes yes yes     43  0\n"
)


def test_cv_fonts():
    """lists the three fonts of the Typst CV, sorted, without prefixes"""
    assert font_names(CV) == ["NewCM10-Bold-Identity-H", "NewCM10-Italic-Identity-H", "NewCM10-Regular-Identity-H"]


def test_prefixes_differ():
    """gives the same names for the same fonts under other prefixes"""
    assert set(font_names(LETTER)) <= set(font_names(CV))


def test_repeated_font_once():
    """lists a font that appears twice only once"""
    twice = HEADER + "ABCDEF+LMRoman10-Regular  Type 1  Custom  yes yes yes  8  0\nGHIJKL+LMRoman10-Regular  Type 1  Custom  yes yes yes  9  0\n"
    assert font_names(twice) == ["LMRoman10-Regular"]


def test_no_prefix_kept():
    """keeps a name without a prefix, and a short prefix, as they are"""
    plain = HEADER + "Helvetica  Type 1  WinAnsi  no no no  5  0\nABC+Font  Type 1  Custom  yes yes yes  6  0\n"
    assert font_names(plain) == ["ABC+Font", "Helvetica"]
A hint

Take output.splitlines()[2:], skip empty lines, and keep line.split()[0]. re.sub(r"^[A-Z]{6}\+", "", name) removes a prefix and leaves any other name alone; a set, then sorted, gives each name once.

The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

Last, a round of questions on the five ideas, from the type filters to the new paper, which opens with the idea you know least well so far:

Practice that adapts to you

Practise this lesson

13 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 13 Which entry type does cv.bib use for a talk, and where does it keep the event and the place?

    Choose one answer.

    Show the answer: Question 1 of 13

    Answer: @unpublished, with the event and the place in its note field

    A type per kind of work is what lets one filter per list sort them apart.

  2. Question 2 of 13 The two journal articles of cv.bib are from 2022 and 2024, written in that order in the file. In what order does the CV list them, and why?

    Choose one answer.

    Show the answer: Question 2 of 13

    Answer: 2024 first, because sorting=ydnt sorts by year, newest first

    A CV is read from the newest work back, so the sort is part of the CV's design, not left to the order in which works were added.

  3. Question 3 of 13 A colleague prints the lists with heading=subbibliography, title={Journal articles} and finds that they look different from the CV's other headings. Why, and what does cv.tex do instead?

    Choose one answer.

    Show the answer: Question 3 of 13

    Answer: subbibliography makes biblatex's own heading, without the style's rule; cv.tex uses \cvsection and heading=none

    Let the style draw every heading: a list's heading then matches the rest of the page by construction.

  4. Question 4 of 13 What would #bibliography("cv.bib", full: true) print?

    Choose one answer.

    Show the answer: Question 4 of 13

    Answer: Every work of cv.bib, cited or not, in one list

    full is the right tool for one list of everything; a CV's lists by type need one bibliography each.

  5. Question 5 of 13 style.typ's works() reads cv.bib with a regular expression. For which kind of file is that enough?

    Choose one answer.

    Show the answer: Question 5 of 13

    Answer: A tidy file of one's own works, whose entries each end with a } alone on a line

    A short reader for a file you control is fine; a file from a reference manager deserves the checks of the lesson's first lab step, or a real parser.

  6. Question 6 of 13 What would the conference papers be numbered if style.typ left out group: none?

    Choose one answer.

    Show the answer: Question 6 of 13

    Answer: They would continue the numbering of the journal articles, from [3], because lists in the default group share one numbering

    group plays the part of resetnumbers: none for numbers that restart in each list, the default for one sequence.

  7. Question 7 of 13 What does #import "style.typ": * give letter.typ?

    Choose one answer.

    Show the answer: Question 7 of 13

    Answer: The definitions of the module, such as look, header and letter, to use by name

    Import and show are two steps: the import makes functions available, the show rule applies the look to the document.

  8. Question 8 of 13 Kavya wants 10.5-point type in all three of her Typst documents. How many files does she change?

    Choose one answer.

    Show the answer: Question 8 of 13

    Answer: One, the set text line of look in style.typ

    That is the point of a shared style: a change to the look is one edit, and the three PDFs follow it.

  9. Question 9 of 13 The LaTeX letter used three fonts and the LaTeX CV six. Does that mean the letter does not match the CV?

    Choose one answer.

    Show the answer: Question 9 of 13

    Answer: No; the letter's three fonts are all among the CV's six, and it simply has no italics, monospace or small capitals

    Compare families, not lists: every font of each document should come from the style's family, as letter-plain.pdf, in Libertinus Serif, did not.

  10. Question 10 of 13 Where does the date of the cover letter come from?

    Choose one answer.

    Show the answer: Question 10 of 13

    Answer: From datetime.today() in Typst and \today in LaTeX, which the sessions fix with SOURCE_DATE_EPOCH

    A letter's date belongs to its build, so it is read from the clock, and fixed for a build that must repeat.

  11. Question 11 of 13 What does letter.typ hold, given that letter() is defined in style.typ?

    Choose one answer.

    Show the answer: Question 11 of 13

    Answer: The recipient's lines, the subject and the text of this letter, and nothing about its look

    What changes from one application to the next goes in the letter's file; everything that should look the same comes from the style.

  12. Question 12 of 13 Kavya adds a paper to cv.bib and rebuilds cv.tex in a folder that still has the old .aux file. What does the biblatex manual warn about?

    Choose one answer.

    Show the answer: Question 12 of 13

    Answer: With defernumbers, a change to the .bib file usually needs the old .aux file deleted before the build

    Delete the auxiliary files, or build in a clean folder, after a change to the data of a CV numbered with defernumbers.

  13. Question 13 of 13 Kavya also wants a two-page CV for teaching posts that lists only works marked teaching. What is the smallest change?

    Choose one answer.

    Show the answer: Question 13 of 13

    Answer: Add teaching to the keywords of those entries, and give the new document a filter on that keyword

    Types say what a work is and keywords what it is for; documents choose with filters, and the data stays one file.

Interview questions

Warm-up (fresher to 2–5 years): How does biblatex print only the journal articles of a CV’s .bib file, under a heading of their own?

With a filter on \printbibliography. \nocite{*} first puts every entry of the file into the bibliography without a citation in the text. Then \printbibliography[type=article, heading=none] prints only the entries of type article, and a heading command of my own, such as \cvsection{Journal articles}, goes before it, or heading=subbibliography with a title. Another \printbibliography with type=inproceedings gives the conference papers, and so on. With sorting=ydnt the newest come first, and with defernumbers=true and resetnumbers=true each list starts again at one. A keyword=selected filter makes a short list for a resume from the same file.

Two harder questions close the module: one source for a researcher’s whole publication record, and three documents that drifted apart. Try each before you look at its first step:

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Mid to senior

    A researcher's CV exists as a word-processor file and a LaTeX file, each with about 40 hand-typed references, and the two now disagree on page ranges and titles. Design one source that groups the works by type, marks a few for a short resume, builds in LaTeX and Typst, and stays right as works are added.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Fresher to mid

    Your resume, CV and cover letter each carry their own copy of the margins, fonts and heading style, and after a year of edits they no longer match. Restructure them around one shared style, and show how you check that the three PDFs really match.

    Model answer, Pro. First step and follow-ups

Key takeaways

  • One .bib file can feed every publication list: \nocite{*}, a type filter per list, sorting=ydnt and numbers reset for each list in LaTeX.
  • Typst builds the same lists with one bibliography per block, each citing its works with form: none, and a small CSL file prints what its built-in style leaves out.
  • A style file or module holds the look and the shared parts, so the resume, the CV and the letter set nothing of their own, and their fonts, read with pdffonts, show whether they match.
  • The cover letter is a function or a few commands of the same style, with only its own words in its file.
  • Adding a paper is one entry in the .bib file; the year sorts it and a keyword puts it on the resume, in both systems.

Flashcards · 8 cards

Recall it, then check

Say the answer to yourself before you look: recalling it is what makes it stick.

  1. How does biblatex list every work of a .bib file without citing each one?

    \nocite{*} puts every entry into the bibliography; sorting=ydnt then sorts them by year, newest first.

  2. How do you print one list per entry type, each numbered from 1?

    One \printbibliography per type, such as [type=article, heading=none] under a heading of your own, with defernumbers and resetnumbers=true.

  3. Which entry type holds a talk in biblatex?

    @unpublished, a work not formally published, with the event and the place kept in its note field.

  4. How does Typst print a CV's separate publication lists?

    One bibliography per block: each work is cited with form: none inside the block, and the block's bibliography targets selector(cite).within(here()); group: none numbers each list from 1.

  5. Why did the Typst CV's talks lose their event and place?

    Typst's IEEE style did not print the note field for those entries; a CSL file of 29 lines that adds the note variable fixed it.

  6. How do you check that a resume, a CV and a letter share one look?

    List their fonts with pdffonts and compare the names with the subset prefixes removed; a document that forgot the style shows other fonts.

  7. Why did letter-plain.typ come out in Libertinus Serif?

    It imported the style but left out #show: look.with(...), so Typst's default font applied, with no error.

  8. How does a new paper reach both the CV and the resume?

    Add one entry to the .bib file: its year sorts it into the CV, and keywords = {selected} puts it on resumes that filter by that keyword.

Terms in this lesson

keywords field
A .bib field of comma-separated words, such as keywords = {selected}, that biblatex's keyword and notkeyword filters read to choose the entries of a list.
module
In Typst, a .typ file whose definitions other files take with #import "style.typ": name, so that several documents share one set of functions and rules.
subset prefix
The six capital letters and plus sign before a font's name in a PDF, as in DSRJEH+NewCM10-Regular, which mark a font embedded with only the glyphs the document uses. They differ from file to file.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.