skip to content

Data Formats: JSON, CSV, and Pickle

Data arrives as JSON, CSV or XML, config as TOML, and caches as pickle — each with its own module API and type-mapping surprises. One of the five must never touch untrusted input.

part ofPythonoverview, primer and where to startread it →
on this pageshow

questions

20

Why is str.split(',') wrong for parsing a CSV line, and what does csv.reader do instead?

level: juniorimportance: must knowfreq 58%

answer

  1. Commas are not always separators
  2. Quoting is what breaks naive parsing
  3. One record is not one line
  4. Doubled quote characters, not backslashes
  5. csv.reader yields lists of str

basics

~20 s

A comma inside a quoted field is data, not a separator, so str.split(',') tears such a row apart. csv.reader runs the quoting state machine over the file object and yields each record as a list of strings.

solid answer

~50 s

`str.split(',')` treats every comma as a separator, but a CSV field may legally contain a comma, a doubled quote character or even a line break as long as the field is quoted. Splitting therefore silently produces the wrong number of columns on exactly the rows that matter, and the damage shows up far downstream. `csv.reader(fileobj)` wraps the file object in a parser that tracks whether it is inside a quoted field, un-doubles embedded quote characters, and pulls extra lines from the underlying iterator when a quoted field spans them. It yields one `list[str]` per record. Every value comes back as `str` - the module does no type conversion unless you ask for `csv.QUOTE_NONNUMERIC`, which converts unquoted fields to `float` on read. Delimiter and quote character are configurable via `delimiter=` and `quotechar=`, so the same parser reads tab- or semicolon-separated files.

code

python · 6 lines
python
import csv
import io

raw = 'city,note\n"Paris, FR","said ""oui"" here"\n"Berlin, DE","two\nlines"\n'
for row in csv.reader(io.StringIO(raw, newline="")):
    print(row)

go deeper

for a junior

Be ready to say why a comma inside a quoted field defeats str.split(',') and to reach for csv.reader by reflex. Remember that every field comes back as a string, so you do your own int or float conversion.

for a middle

Explain the mechanics: the reader is a lazy iterator over the file object, it un-doubles embedded quote characters, and it pulls extra lines when a quoted field spans them. Know delimiter, quotechar and the QUOTE_ constants by name.

for a senior

Show judgement about data you did not produce: quoting anomalies, ragged rows and files whose dialect differs from the one documented. Say when you would register a named dialect rather than repeat keyword arguments across a codebase.

for a principal

Own the boundary question: the csv module gives you correct record shape and nothing else. Decide where schema validation, typing and rejection of bad rows live, and whether a self-describing format would serve the pipeline better than CSV at all.

### The failure mode CSV looks like a format you can parse with one line of string manipulation, and for hand-made sample data you can. The trouble is that the files you actually receive are produced by spreadsheets, database exports and other people's programs, all of which quote a field the moment it contains something structural. A row such as `"Paris, FR",7` has two fields; `str.split(',')` on that text returns three (`'"Paris'`, `' FR"'`, `'7'`), with a stray quote character welded onto two of them. Nothing raises. The row simply becomes wrong, and it stays wrong until some later stage trips over a column count or a value that will not convert. Three things a quoted field can hold defeat naive splitting: - **The delimiter itself.** `"Paris, FR"` is one field containing a comma. - **The quote character.** It appears doubled inside the field: `"said ""oui"" here"` is the single value `said "oui" here`. Note that the convention is doubling, not backslash escaping - a candidate who reaches for `\"` is thinking of a different format. - **A line break.** A quoted field may contain a newline, which means one *record* is not the same thing as one *line* of the file. Any approach built on iterating lines and splitting them is structurally unable to handle this. ### What csv.reader actually is `csv.reader(f)` takes any iterator of strings - usually an open text file, sometimes an `io.StringIO`, sometimes a generator - and returns a reader object that is itself an iterator. Each `next()` consumes as many strings from the source as the record needs and returns a `list` of the record's fields. The parsing is done by a C state machine, so it is both correct and considerably faster than a Python-level loop. Because the reader is a lazy iterator over the source, it never holds more than the current record in memory, and it is only valid while the underlying file object is open. Returning a reader out of a `with open(...)` block is a classic bug: the file closes and the next iteration raises `ValueError`. ### Types, and the absence of conversion Every field arrives as `str`, including `'7'` and `''`. An empty field is the empty string, not `None`. This is deliberate: the module refuses to guess. If you want numbers you convert them yourself, which is also where you get to decide what a malformed value should do. The one built-in exception is `csv.QUOTE_NONNUMERIC`, which on *reading* converts every unquoted field to `float` and raises `ValueError` if that fails. ### The writing side `csv.writer(f).writerow(seq)` is the mirror image: it applies quoting so that the row can be read back correctly. The `quoting=` argument takes the module's constants and decides how aggressive that is. `csv.QUOTE_MINIMAL`, the default, quotes only fields that need it - those containing the delimiter, the quote character or a newline. `csv.QUOTE_ALL` quotes everything, which is useful when a downstream consumer is itself sloppy. `csv.QUOTE_NONE` quotes nothing and raises if a field would need quoting, unless you supply an `escapechar=`. `csv.QUOTE_NONNUMERIC` quotes every non-numeric field. Python 3.12 added two more, `csv.QUOTE_STRINGS` and `csv.QUOTE_NOTNULL`, which differ in how they treat `None` versus the empty string on output. ### Dialects The bundle of delimiter, quote character, quoting policy, line terminator and whitespace handling is a *dialect*. `csv.excel` is the default; `csv.excel_tab` and `csv.unix_dialect` ship alongside it, and `csv.register_dialect` names your own so that reader and writer cannot drift apart. Pass either the dialect name as the second argument or the individual keywords. `csv.Sniffer` will guess a dialect from a text sample - handy for exploratory work, unreliable enough that you should not put it in an unattended pipeline. ### The honest limits The module parses; it does not validate. Ragged rows come back as short or long lists without complaint (unless you use `csv.DictReader`, which has `restkey=` and `restval=` for exactly this). It has no schema, no header semantics beyond what you impose, and no notion of types. What it buys you is that the shape of every record is correct, which is precisely the thing hand-rolled splitting cannot promise. ### Where the reader gets its input One detail pays off later: `csv.reader` does not require a file. It accepts any iterator of strings, which means a list of lines, an `io.StringIO`, a generator that decodes a network stream, or a generator that filters comment lines out before the parser sees them all work identically. That is the idiomatic way to skip a preamble - wrap the file in a generator expression that drops the junk lines and hand the generator to `csv.reader` - and it is far safer than trying to strip the preamble with string surgery afterwards, because the parser still owns the record structure. The one hard constraint is that those strings must be `str`. Opening the file in binary mode and passing it to `csv.reader` fails, because the parser has no encoding to apply. Decoding is a separate layer's job, and keeping it separate is what lets the same reader code serve a file, a socket and a test fixture without change.

  • What Python types do the fields of a csv.reader row have?
    All of them are `str`, including digits and empty fields - an empty field is `''`, never `None`. The module does no conversion, so you cast columns yourself and decide what a bad value means. The single exception is `csv.QUOTE_NONNUMERIC`, which on reading converts unquoted fields to `float` and raises `ValueError` when one will not parse.
  • What does csv.QUOTE_MINIMAL do differently from csv.QUOTE_ALL when writing?
    `csv.QUOTE_MINIMAL` is the writer default and quotes a field only when it has to - when the field contains the delimiter, the quote character, or a line break. `csv.QUOTE_ALL` wraps every field regardless. Neither changes what a correct reader gets back; QUOTE_ALL just produces larger files that survive sloppier downstream consumers.
  • How do you read a semicolon-separated or tab-separated file with the same code?
    Pass `delimiter=';'` or `delimiter='\t'` to `csv.reader`, or name a dialect. The module's parser is not comma-specific - delimiter, `quotechar=`, quoting policy and line terminator are all dialect settings. Registering a named dialect with `csv.register_dialect` is better than repeating keywords, because it keeps the reader and writer of the same file in step.

Splitting on commas is like cutting a sentence at every space and expecting to get words back - it works until someone writes a phrase in quotation marks and you slice straight through it.

saying these in an interview costs you the question

  • Claims split(',') is fine when the data is clean
  • Thinks csv.reader converts numeric columns to int or float
  • Believes a CSV field can never contain a newline
  • Escapes an embedded quote with a backslash instead of doubling it
  • Writes a regular expression to parse quoted fields
  • Assumes an empty field comes back as None

context

open as a page

How do json.load, json.loads, json.dump and json.dumps differ?

level: juniorimportance: must knowfreq 75%

basics

~20 s

The trailing s means string. json.loads parses JSON text already in memory and json.load reads it from an open file object; json.dumps returns a JSON string, while json.dump writes that text straight into a file object.

open as a page

Why does pickle.loads() run arbitrary code when handed untrusted bytes?

level: juniorimportance: must knowfreq 70%

basics

~20 s

A pickle stream is a small program for a stack machine, not a data document. Its opcodes can import any importable name and call it, so pickle.loads on attacker-controlled bytes runs the attacker's code before you inspect any value.

open as a page

Why does tomllib.load require a file opened in binary mode ('rb')?

level: juniorimportance: must knowfreq 50%

basics

~20 s

tomllib.load reads bytes and decodes them as UTF-8 itself, because a TOML document is UTF-8 by definition. A file opened in text mode has already been decoded with the platform encoding, so load raises TypeError; open the path with 'rb'.

open as a page

How do Element.find, findall and iter differ in xml.etree.ElementTree?

level: juniorimportance: must knowfreq 45%

basics

~20 s

Element.find returns the first match or None, findall returns a list of every match at that path, and iter walks the whole subtree recursively, including the element itself, yielding each element with the given tag.

open as a page

Why must a file passed to csv.writer or csv.reader be opened with newline=''?

level: middleimportance: must knowfreq 46%

basics

~20 s

The csv module handles line endings itself. Opening with newline='' switches off the text layer's newline translation so the writer's own terminator is not rewritten and a newline embedded in a quoted field is not corrupted on read.

open as a page

How do you make json.dumps serialize a datetime or a Decimal?

level: middleimportance: must knowfreq 66%

basics

~20 s

Give the encoder a fallback: pass default=fn, a callable json.dumps invokes only for objects it cannot handle, or subclass json.JSONEncoder, override default and pass it as cls=. Return a substitute — isoformat() for a datetime, str() for a Decimal.

open as a page

Why does ElementTree's findall('txn') match nothing when the XML declares xmlns?

level: middleimportance: must knowfreq 50%

basics

~20 s

The parser expands every namespaced tag into Clark notation, so the element is named {uri}txn and the bare name txn never matches. Search with a prefix-to-URI map, findall('t:txn', {'t': uri}), or write the {uri}txn form yourself.

open as a page

When would you choose csv.DictReader and csv.DictWriter over csv.reader and csv.writer?

level: middleimportance: should knowfreq 44%

basics

~20 s

Use the dict-based pair when the file has a header row and you want fields addressed by column name rather than position, so a reordered column cannot break the code. Keep the list-based pair for headerless files and the hottest loops.

open as a page

How does json.dumps map Python types, and what does it refuse?

level: middleimportance: should knowfreq 60%

basics

~20 s

dict becomes an object; list and tuple both become arrays; str becomes a string; int and float become numbers; True, False and None become true, false and null. Everything else — set, bytes, datetime, Decimal — raises TypeError.

open as a page

What does pickle actually store for a class instance, and what can it not store?

level: middleimportance: should knowfreq 50%

basics

~20 s

For an ordinary instance pickle stores a reference to its class by module and qualified name, plus the instance state, normally its dict. No code is stored, so live OS resources and unnamed callables cannot be pickled.

open as a page

In configparser, what does the DEFAULT section do and how does interpolation expand a value?

level: middleimportance: should knowfreq 40%

basics

~20 s

configparser's DEFAULT section holds options every other section inherits, and it never shows up in sections(). BasicInterpolation then expands %(name)s inside a value on read, resolving the name in that section first and DEFAULT second.

open as a page

Why must you call configparser's getboolean() instead of bool() on a config value?

level: middleimportance: should knowfreq 45%

basics

~20 s

Every value configparser returns is a str, and bool() of any non-empty string is True — so bool('false') is True. getboolean() maps a fixed vocabulary (1/yes/true/on and 0/no/false/off, case-insensitively) to a real bool and raises ValueError on anything else.

open as a page

How do you build an XML document with ElementTree's SubElement and write it out?

level: middleimportance: should knowfreq 30%

basics

~20 s

Create the root with ET.Element, add children with ET.SubElement, which attaches them for you, set .text and attributes as strings, optionally pretty-print with ET.indent, then wrap the root in ET.ElementTree and call write with an encoding and xml_declaration=True.

open as a page

A geocoding batch streams a 6 GB CSV with csv.DictReader and writes each result as its lookup finishes - how do you keep memory flat and the output rows correctly matched?

level: seniorimportance: should knowfreq 36%

basics

~20 s

Iterate the reader lazily and never materialise it with list(), keeping one record alive at a time. Never rely on output position matching input position: carry a stable key column from each input record into the output row you write.

open as a page

How do you diagnose a json.JSONDecodeError in a production ingest path?

level: seniorimportance: should knowfreq 44%

basics

~20 s

json.JSONDecodeError subclasses ValueError and carries msg, doc, pos, lineno and colno. Catch that class specifically, log the message plus a short slice of doc around pos rather than the whole payload, and keep malformed text separate from valid-but-wrong-shape data.

open as a page

A pickle.Pickler reused for millions of genome records makes memory grow without bound — what is holding them?

level: seniorimportance: should knowfreq 28%

basics

~20 s

The pickler's memo. A pickle.Pickler holds a strong reference to every object it has already written, and that table lives as long as the Pickler, not as long as one dump() call. Call clear_memo(), or use a fresh Pickler.

open as a page

A video-metadata extractor loads settings with tomllib and lets os.environ override them per deployment — how do you build that layering so a bad override fails at startup?

level: seniorimportance: should knowfreq 40%

basics

~20 s

Resolve the layers once at startup: parse the TOML with tomllib, look up a prefixed os.environ name per key, coerce that string to the type the file declared, then validate the merged result and abort before any work begins.

open as a page

ElementTree.parse OOMs on a 6 GB XML feed; how do you stream it with iterparse?

level: seniorimportance: should knowfreq 35%

basics

~20 s

ET.parse materialises the entire document as objects, so peak memory is a multiple of the file size. ET.iterparse yields (event, element) pairs as parsing proceeds: handle each record on its end event, clear it, and unhook it from the root so memory stays flat.

open as a page

What changes between pickle protocol versions, and when would you pin one explicitly?

level: middleimportance: nice to knowfreq 22%

basics

~20 s

Protocol versions change the encoding and which object features are available, not the safety of loading. Newer interpreters read older protocols, but an older one cannot read a newer stream — pin protocol= when the reader may lag.

open as a page