Grokking the Data Engineering Interview
Vote

0% completed

​

Parsing Messy Files

The Vendor File

CSV Quoting

DictReader and the Wrong Number of Fields

JSON Lines

Reject, Repair, or Quarantine

Using This in an Interview

Key Takeaways

Practice Questions

A freight vendor sends a shipment file every morning. The short script below reads the first two shipments, splits each line at every comma, and adds up the weights:

Python3
Python3

. . . .

Output:

5 fields: {'shipment_id': 'S-301', 'consignee': 'Harlow Supply', 'pallets': '4', 'weight_kg': '812', 'note': ''} 6 fields: {'shipment_id': 'S-302', 'consignee': '"Brandt', 'pallets': ' Keller and Sons"', 'weight_kg': '2', 'note': '455'} total weight_kg: 814

The true total is 812 + 455 = 1,267 kilograms, but the script reports 814. The consignee of S-302 contains a comma, so the vendor wrapped that name in double quotes. The split ignored the quotes and cut the line into six pieces, which moved every later value one column to the right. The pallet count, 2, became the weight, and nothing raised an error.

This lesson covers how to read such files correctly, how to read a format with one record per line, and what to do with the rows that still cannot be read.

The Vendor File

The complete file has a header line and eleven more lines. Every script below contains this text, so each one runs on its own:

shipment_id,consignee,pallets,weight_kg,note S-301,Harlow Supply,4,812, S-302,"Brandt, Keller and Sons",2,455,dock 3 S-303,Ostrava Tools,3,610,"call before delivery" S-304,"Mira Foods ""Fresh"" Line",1,130, s-305 ,Pell Hardware,5,725, S-306,"Vasko, Lind and Partners",two,388, S-307,Northgate Paper,2 shipment_id,consignee,pallets,weight_kg,note S-308,Corla Garden,6,"1,250",

Several lines have a defect. S-303 has a note that continues onto a second line, and S-304 has quotes inside its name. S-305 has spaces around a lowercase ID, and S-306 spells its pallet count as a word. S-307 stops after three fields, a blank line and a second header follow it, and S-308 writes its weight with a thousands separator. The second header suggests that two files were joined into one.

CSV Quoting

A CSV (comma-separated values) file stores one record per line with fields separated by commas, and quoting is the rule that lets a field contain a comma, a quote, or a line break. RFC 4180, the document that describes the format, says such fields "should be enclosed in double-quotes." A double quote inside a quoted field is written twice, so "Mira Foods ""Fresh"" Line" holds the value Mira Foods "Fresh" Line. Python's csv module applies these rules, and its documentation warns that the format has no "well-defined standard", so files from different programs differ in small ways.

Here is the same file read with csv.reader. The script prints each record with its field count and the reader's line_num:

Python3
Python3

. . . .

Output:

line 2: 5 fields ['S-301', 'Harlow Supply', '4', '812', ''] line 3: 5 fields ['S-302', 'Brandt, Keller and Sons', '2', '455', 'dock 3'] line 5: 5 fields ['S-303', 'Ostrava Tools', '3', '610', 'call before\ndelivery'] line 6: 5 fields ['S-304', 'Mira Foods "Fresh" Line', '1', '130', ''] line 7: 5 fields [' s-305 ', 'Pell Hardware', '5', '725', ''] line 8: 5 fields ['S-306', 'Vasko, Lind and Partners', 'two', '388', ''] line 9: 3 fields ['S-307', 'Northgate Paper', '2'] line 10: 0 fields [] line 11: 5 fields ['shipment_id', 'consignee', 'pallets', 'weight_kg', 'note'] line 12: 5 fields ['S-308', 'Corla Garden', '6', '1,250', ''] records after the header: 10 | lines read: 12

S-302 now has five fields and a weight of 455. The note of S-303 keeps its line break, so one record used lines 4 and 5. That is why the reader reports 12 lines read but returns 11 records, the header included. The documentation states the same difference: line_num is "not the same as the number of records returned." After a record is read, line_num holds the number of its last line, which is why S-303 shows line 5. When an error message names a position in the file, it should give the line number, because a person will open the file in an editor that counts lines.

A quoted comma, split two ways
A quoted comma, split two ways

The reader fixed the quoting but did not judge the values. S-305 still has spaces, S-306 still says two, and S-307 still has three fields. A real file adds one more requirement: open it with newline='', because without it "newlines embedded inside quoted fields will not be interpreted correctly."

DictReader and the Wrong Number of Fields

csv.DictReader returns each record as a dictionary keyed by the header names. It does not reject a record of the wrong width. Missing fields are filled with None, and extra fields go into a list stored under the key None:

Python3
Python3

. . . .

Output:

{'shipment_id': 'S-307', 'consignee': 'Northgate Paper', 'pallets': '2', 'weight_kg': None, 'note': None} {'shipment_id': 'S-309', 'consignee': 'Dunmore Glass', 'pallets': '3', 'weight_kg': '540', 'note': 'fragile', None: ['dock 7']}

A loader that only reads row['weight_kg'] would see None for S-307 and could store it as a missing weight. Checking the field count against the header is the job of the loader, and it must happen before any value is used.

When the pipeline writes CSV, csv.writer handles quoting in the other direction. Its default mode, QUOTE_MINIMAL, quotes only fields that contain the delimiter, the quote character, or a line break.

JSON Lines

JSON Lines is a format in which every line of the file is one complete JSON value, usually one object per record. The format's specification requires that "each line is a valid JSON value", and it adds that a blank line is not one. Because the lines are independent, a reader parses each line with json.loads and a defect damages one record only.

The vendor also sends a tracking feed in this format. Line 3 lost its closing brace, and line 4 contains NaN:

Python3
Python3

. . . .

Output:

parsed: ['S-301', 'S-302', 'S-305'] line 3: Expecting ',' delimiter at column 66 line 4: weight_kg is nan as one JSON array: Expecting property name enclosed in double quotes at line 4, no records parsed

Three of the five records load. The exception, json.JSONDecodeError, carries the message and the column, so the error report can say where to look. The last line shows the alternative: the same five objects stored as one JSON array give one error and zero records, because json.loads must parse the array as one document.

Line 4 shows a behavior of Python's json module that lets a bad value through. It accepts NaN and Infinity as floats, which the documentation calls "outside the JSON spec." The line parses without an error, so the script checks math.isfinite before it accepts the weight. Parsing a line proves only that the text is valid JSON. It does not prove that the values make sense.

Reject, Repair, or Quarantine

Handling a bad record means giving it exactly one outcome and counting that outcome. A parser only splits text into fields, so every record it returns still needs this decision. There are three outcomes for a bad record:

  • Reject: drop the record and count it. This fits records that carry no data, such as the blank line and the repeated header.
  • Repair: change the value by a fixed rule and load it. This fits defects with exactly one correct reading, such as the spaces in s-305 or the separator in 1,250.
  • Quarantine: keep the record unchanged in a separate file, with the reason and the line number, and load the rest. This fits records that need a person or the vendor to decide, such as two pallets or a missing weight.
Ten records, three outcomes
Ten records, three outcomes

The script below applies the three outcomes to the vendor file and counts each one. A record that fails a check is quarantined with every failed field and its value as the reason:

Python3
Python3

. . . .

Output:

{'read': 10, 'loaded': 6, 'repaired': 2, 'quarantined': 2, 'rejected': 2} total weight_kg loaded: 3982 quarantine share 20%: load S-306,"Vasko, Lind and Partners",two,388,,line 8: pallets='two' S-307,Northgate Paper,2,line 9: 3 fields

The counts must add up: 6 loaded + 2 quarantined + 2 rejected = 10 records read. The assert stops the job when they do not, because a record that leaves the loop without being counted is a record that disappeared. The repaired count, 2, is a subset of the loaded count. The loaded weight is 812 + 455 + 610 + 130 + 725 + 1,250 = 3,982 kilograms.

The quarantine file is itself a CSV, written with csv.writer. The writer quoted Vasko, Lind and Partners again, so the file can be read back with the same reader. The reason column at the end lets the vendor see what to fix.

The script also stops the whole file when too many records fail. Here 2 of 10 records, or 20 percent, went to quarantine, which is under the 25 percent limit, so the six good records load. A file in which most records fail can mean that the vendor changed the format, such as adding a column, and loading the few records that pass would produce a partial and misleading table.

Each outcome has a cost:

  • Cost of rejecting: the data is gone. If a rule rejects a record that held real data, nobody can recover it later. Reject only records that hold no data, and always count them.
  • Cost of repairing: a wrong rule changes data without notice. Trimming spaces from an ID is safe, but reading two as 2 is a guess. A repair is acceptable only when there is exactly one correct reading, and the job should count every repair.
  • Cost of quarantining: someone must review the file. Quarantined records are missing from today's totals until the vendor sends a fix, and the file must have an owner. It is the better choice whenever the correct value is unknown.
  • Cost of failing the file: nothing loads that day. It is the better choice when the failure share suggests a format change, because then even the passing records may be wrong.
RejectRepairQuarantineFail the file
Record loadedNoYes, changedNo, kept asideNo record loads
Data recoverable laterNoOriginal value lost unless loggedYesYes, from the source file
FitsBlank lines, repeated headersOne correct readingUnknown correct valueA format change
Must be countedYesYesYes, with reason and lineYes, with the failure share

Using This in an Interview

Amazon's interview prep page for data roles says its Data Engineer technical interviews combine verbal and coding exercises. A parsing task fits both parts, because the code is short and the decisions about bad rows need explaining. While you write the parser, say three things: you use the csv module because fields can hold quoted commas, you check the field count before you read a value, and you count every record as loaded, quarantined, or rejected.

  • "What if the file is too large to read into memory?" csv.reader and a loop over a file's lines both read one record at a time, so memory stays small. Write the quarantine records to a file as they appear instead of keeping them in a list.
  • "How do you know the job did not lose records?" Assert that the records read equal loaded plus quarantined plus rejected, and log all four counts. For JSON Lines, compare the line count of the file with the same sum.

Key Takeaways

  • Splitting a CSV line on commas gives wrong fields when a quoted value contains a comma; csv.reader follows the quoting rules, including doubled quotes and line breaks inside a field.
  • line_num counts lines, not records, and DictReader fills short rows with None, so check the field count yourself.
  • JSON Lines keeps one value per line, so a bad line loses only that record; Python's json module accepts NaN, so validate values after parsing.
  • Give every bad record one outcome: reject records with no data, repair only when one reading is correct, and quarantine the rest with a reason and a line number.
  • Count every outcome, assert that the counts add up to the records read, and fail the whole file when the failure share suggests a format change.

The six loaded shipments are now clean records with integer weights. The next lesson, Grouping and Aggregating in Python, totals records like these by key with defaultdict, Counter, and itertools.groupby.

Practice Questions

1. A vendor line is S-310,"Orin, Bell",3,"2,400",. How many fields does line.split(",") return, and how many does csv.reader return?

<details> <summary>Show answer</summary>

split returns 7 fields, and csv.reader returns 5. The split cuts at all six commas, including the one inside "Orin, Bell" and the one inside "2,400". The reader treats each quoted value as one field and removes the quotes, so the weight field is the text 2,400. A repair rule must still remove the separator before converting it to an integer.

</details>

2. A loader opens the vendor file without newline='' and reads it with csv.reader. Which record is at risk, and why?

<details> <summary>Show answer</summary>

S-303, whose note contains a line break inside quotes. The Python documentation says that without newline='', "newlines embedded inside quoted fields will not be interpreted correctly." Only records with a line break inside a quoted field are affected, which is why the defect can go unnoticed until one appears.

</details>

3. A file has 1,000 records after the header. The job loads 940, quarantines 35, and rejects 20. What is wrong, and what should the job do?

<details> <summary>Show answer</summary>

Five records are not counted: 940 + 35 + 20 = 995. A record left the loop without an outcome, usually because a code path such as an exception handler skipped it without counting. The job should fail the run instead of loading, because the missing records are not in any table and not in the quarantine file.

</details>

4. A JSON Lines feed has the line {"shipment_id": "S-311", "weight_kg": Infinity}. Does json.loads raise an error? What should the loader do?

<details> <summary>Show answer</summary>

No error is raised; the weight loads as the float inf. Python's json module accepts NaN, Infinity, and -Infinity, although the JSON specification does not. The loader should test the value with math.isfinite and quarantine the record with the reason, as the lesson's script does for NaN.

</details>

5. A shipment has the pallet count two. A teammate proposes a repair that maps number words to digits. What is the argument against it?

<details> <summary>Show answer</summary>

The value has more than one possible reading, so a repair would be a guess. The vendor may have meant 2, or the column may have shifted, which is the same symptom a format change produces. Quarantining the record with the reason pallets='two' keeps the original text and lets the vendor confirm the value. A safe repair, such as trimming spaces from an ID, has only one correct result.

</details>

Reading Progress

0%


Vote for new content

On This Page

The Vendor File

CSV Quoting

DictReader and the Wrong Number of Fields

JSON Lines

Reject, Repair, or Quarantine

Using This in an Interview

Key Takeaways

Practice Questions