Csv
Standard library namespace · Always available, with no import
CSV is a simple way to write a table as text: one row to a line, with commas between the cells. It is
what spreadsheets export and what many small programs save. Csv reads it and writes it.
There are two ways to read it, depending on whether you know the columns ahead of time.
When you know them, describe a row with a struct, and Csv.decode turns the text into a
list of them. Csv.encode goes the other way:
struct Score { const name: String const points: Int const team: String? = nothing}
const scores = Csv.decode("name,points,team\nAda,120,red\nGrace,95,", as: List[Score])print(scores[0].points + scores[1].points)print(Csv.encode(scores))215name,points,teamAda,120,redGrace,95,When you don’t, Csv.parse gives every row as a list of text, and
Csv.parse_records uses the first row as names for the columns:
const rows = Csv.parse_records("name,age\nAda,36\nGrace,45")for row in rows { print("#{row["name"].or("?")} is #{row["age"].or("?")}")}Ada is 36Grace is 45CSV text can have a leading byte-order mark, Unix or Windows line endings, cells in quotes that hold
commas or line breaks, and a doubled quote ("") for a quote inside a quoted cell. All are read
correctly. Every function takes an optional separator:, one character, for files that use ; or a
tab instead of a comma.
At a glance
Section titled “At a glance”| Reading | |
|---|---|
Csv.parse(text, separator:) |
Every row, as a list of text |
Csv.parse_records(text, separator:) |
Each row as a dictionary, keyed by the header |
Csv.decode(text, as:, separator:) |
Each row as a struct of your own |
| Writing | |
|---|---|
Csv.format(rows, separator:) |
Rows of text as CSV |
Csv.encode(records, separator:) |
A list of structs as CSV, with a header |
Reading
Section titled “Reading”Csv.parse(text: String, separator: String = ","): List[List[String]]
Every row as a list of text cells, including a header row if the text has one. Rows can have different
lengths, since parse gives no meaning to columns. An empty text gives an empty list, and a line break
at the very end doesn’t add an empty row.
print(Csv.parse("a,b\n1,2\n"))print(Csv.parse("a;b\n1;2", ";"))print(Csv.parse("\"x, y\",\"say \"\"hi\"\"\"\n\"two\nlines\",z"))[["a", "b"], ["1", "2"]][["a", "b"], ["1", "2"]][["x, y", "say \"hi\""], ["two\nlines", "z"]]Raises a CsvError for a quoted cell that never closes, or a separator that isn’t one
character.
Csv.parse_records(text: String, separator: String = ","): List[Dict[String, String]]
Uses the first row as the names of the columns, and gives each later row as a dictionary from those names to its cells, which are text. The dictionaries keep the columns in their order in the text.
const people = Csv.parse_records("name,age\nAda,36\nGrace,45")print(people)print(people[1]["name"])[["name": "Ada", "age": "36"], ["name": "Grace", "age": "45"]]GraceAn empty text has no records.
Raises a CsvError when a header name is blank or repeated, or a row doesn’t have as
many cells as the header.
Csv.decode(text: String, as: Type, separator: String = ","): Type
Reads rows into a list of your own struct, such as as: List[Score]. The first row names the columns,
and each name is matched to a field of the struct. Columns the struct doesn’t have are ignored.
A field can be a String, Int, Float, Bool, an enum, a Date, Time, DateTime, or Instant, or
an optional of one of those. A Bool can be true or false in any capitals.
enum Color { red, blue}
struct Row { const day: Date const color: Color const active: Bool const ratio: Float}
const rows = Csv.decode("day,color,active,ratio\n2026-09-25,red,TRUE,2\n", as: List[Row])print(rows)[Row(day: 2026-09-25, color: Color.red, active: true, ratio: 2.0)]An empty cell is "" for a String, nothing for an optional, and the field’s default when it has one.
An empty cell for any other field is an error, and so is a column the struct needs and the text lacks,
unless the field has a default or is optional. Private fields are never read, so they need defaults.
as: takes a type written out in the program, not a value, so decode must be written as
Csv.decode(...), and can’t be stored in a name or passed along as a function.
Raises a CsvError for a missing required column, or a cell that can’t become its
field’s type. The message names the line and the column.
Writing
Section titled “Writing”Csv.format(rows: List[List[String]], separator: String = ","): String
Rows of text written as CSV, with a line break between rows and none at the end. A cell is put in quotes only when it has to be, to keep its meaning: when it contains the separator, a quote, or a line break, or starts or ends with a space.
const rows = [["a", "b"], ["x, y", "say \"hi\""], [" pad", ""]]print(Csv.format(rows))print(Csv.format([["a", "b"], ["1", "2"]], ";"))a,b"x, y","say ""hi"""" pad",a;b1;2Csv.encode(records: List[Record], separator: String = ","): String
A list of structs written as CSV: a first row of the fields’ names, in the order they are declared, then a row for each struct. An empty list still gives the header. Private fields are never written.
The fields can be the same kinds decode reads. An optional that is nothing is an empty cell, an enum
is written as its value’s name, dates and times as they print, and a Float as Emerald shows it, so
2.0 stays 2.0.
struct Score { const name: String const points: Int const team: String? = nothing}
const none: List[Score] = []print(Csv.encode([Score("Ada", 120)]))print(Csv.encode(none))name,points,teamAda,120,name,points,teamA struct with a field CSV can’t hold, such as a List, is caught before the program runs.