This cheatsheet covers how to write and read CSV so that commas, quotes and line breaks inside a field survive a round trip. It is for developers who export spreadsheets, import customer data or parse a file that "works on my machine". CSV has no single master specification, so the confusion it clears up is which rules come from RFC 4180 and which are only habits of Excel and your parsing library. For what the format is, see the CSV glossary page.
| Rule | What the RFC says |
|---|---|
| Record separator | CRLF (bytes 0D 0A). Some programs use LF only |
| Last record | May or may not end with a line break |
| Header line | Optional, same format as a record, signaled by `header=present` or `header=absent` |
| Field count | Each line should have the same number of fields |
| Spaces | Part of the field. Do not trim them |
| Quotes around a field | Optional. Required if the field holds a comma, a double quote or a line break |
| Quote inside a quoted field | Doubled: `"` becomes `""` |
| Quotes in an unquoted field | Not allowed |
| Trailing comma | The last field must not be followed by a comma |
| Media type | `text/csv`, optional parameters `charset` and `header` |
RFC 4180 is category Informational, published October 2005. It describes common practice and does not define an Internet standard.
| Delimiter | Name | Typical source |
|---|---|---|
| `,` | CSV | RFC 4180, most APIs and databases |
| `;` | Semicolon-separated | Spreadsheets in locales where the comma is the decimal separator |
| tab | TSV | Database dumps, copy-paste from sheets |
| vertical bar | Pipe-separated | Legacy exports where text contains commas |
| Field value | Written as |
|---|---|
| `plain` | `plain` |
| `Smith, Jane` | `"Smith, Jane"` |
| `said "hi"` | `"said ""hi"""` |
| two lines | `"line1` newline `line2"` |
| empty string | nothing between the commas |
| Constant | Behavior when writing |
|---|---|
| `QUOTE_MINIMAL` | Quote only fields with the delimiter, quote character or line break (default) |
| `QUOTE_ALL` | Quote every field |
| `QUOTE_NONNUMERIC` | Quote every non-number. When reading, unquoted fields become floats |
| `QUOTE_NONE` | Never quote. Needs an escape character or it raises an error |
import csv, io
data = 'id,name,note\r\n1,"Smith, Jane","said ""hi"""\r\n2,Bob,"line1\r\nline2"\r\n3,,\r\n'
for row in csv.reader(io.StringIO(data, newline='')):
print(row)
['id', 'name', 'note']
['1', 'Smith, Jane', 'said "hi"']
['2', 'Bob', 'line1\r\nline2']
['3', '', '']
Every value comes back as a string, including numbers. The quoted line break stays inside one field. Open real files with newline='' so Python does not rewrite the embedded line breaks.
import csv, io
buf = io.StringIO()
w = csv.writer(buf)
w.writerow(['id', 'name', 'note'])
w.writerow([1, 'Smith, Jane', 'said "hi"'])
w.writerow([2, 'Bob', 'a\nb'])
w.writerow([3, '', None])
print(repr(buf.getvalue()))
'id,name,note\r\n1,"Smith, Jane","said ""hi"""\r\n2,Bob,"a\nb"\r\n3,,\r\n'
The writer quotes only when needed, ends rows with CRLF and writes None as an empty field. Never build CSV with string concatenation.
import csv, io
s = 'name;price\nTea;1,50\n'
print(list(csv.reader(io.StringIO(s))))
print(list(csv.reader(io.StringIO(s), delimiter=';')))
print(csv.Sniffer().sniff(s).delimiter)
[['name;price'], ['Tea;1', '50']]
[['name', 'price'], ['Tea', '1,50']]
;
The default comma delimiter splits the price in half and leaves the header as one column. Pass delimiter=';', or use Sniffer to guess it from a sample.
import csv, io
raw = b'\xef\xbb\xbfid,name\n1,Ann\n'
print(next(csv.reader(io.StringIO(raw.decode('utf-8')))))
print(next(csv.reader(io.StringIO(raw.decode('utf-8-sig')))))
['\ufeffid', 'name']
['id', 'name']
Decoding with utf-8 leaves the BOM glued to the first header, so a lookup of id fails. Decode with utf-8-sig.
import csv, io
for r in csv.DictReader(io.StringIO('a,b\n1,2\n3\n4,5,6\n')):
print(r)
{'a': '1', 'b': '2'}
{'a': '3', 'b': None}
{'a': '4', 'b': '5', None: ['6']}
A short row fills missing columns with None. A long row puts the extras under the key None. Check for both before trusting the data.
import csv, io
def safe(v):
v = str(v)
return "'" + v if v[:1] in ('=', '+', '-', '@', '\t', '\r') else v
b = io.StringIO(); w = csv.writer(b, lineterminator='\n')
w.writerow(['name', 'comment'])
w.writerow(['x', '=HYPERLINK("http://example.com","click")'])
w.writerow(['y', safe('=1+2')])
print(b.getvalue(), end='')
name,comment
x,"=HYPERLINK(""http://example.com"",""click"")"
y,'=1+2
Row x shows the problem: quoting alone does not stop a spreadsheet from treating the cell as a formula. Row y shows the sanitized version, which the spreadsheet reads as text.
'1,"Smith, Jane",ok'.split(',') returns [ '1', '"Smith', ' Jane"', 'ok' ], four pieces instead of three. The same mistake in awk counts 4 fields for a,"b,c",d. Use a real CSV parser.;, and a comma decimal such as 1,50 then looks like two columns in a comma parser. Detect or ask for the delimiter.007 is text in the file, but a spreadsheet may show 7. Parse such columns as strings, and note that 1e3 is also just text until something converts it.id and every lookup of id misses. Decode as utf-8-sig.=, +, - or @ can run as a formula when the export is opened in a spreadsheet. OWASP also lists tab, carriage return and line feed, and full-width variants of those characters in some locales. Quoting is not enough, and OWASP notes that quote-based fixes can fail after Excel saves the file again.newline='' in Python: line breaks inside quoted fields are translated, which corrupts multi-line values.header parameter or to you. Decide explicitly and document it.text/csv and add ;charset=utf-8 when the encoding is not US-ASCII. W3C advises UTF-8 as the default encoding.text/csv fits among other media types