LESSON 67 · DEEPER PYTHON31 min

Validate CSV Rows with DictReader

Read CSV by field name and validate its header, text, and numeric fields before using a row.

Example check: Worked and guided code samples are checked against CPython 3.13. Practice uses Pyodide 0.29.5 (Python 3.13) in your browser. How we check lessons

Video lesson: Validate CSV Rows with DictReader

The preview is stored on this site. YouTube loads only when you press Play. Watch on YouTube

Read the video transcript

Select a timestamp to watch that moment on YouTube.

  1. 00:00

    A CSV file looks so simple that it is tempting to split each line at a comma. That shortcut survives many quick demonstrations because the first few names contain no punctuation. Then a real product named Pens, blue arrives. The product name is quoted as one CSV field, but a manual split sees its internal comma and shifts every following value. An import that does not fail clearly might even attach the wrong quantity to the wrong item. In this lesson we will reproduce that failure, then use Python's csv.DictReader to follow CSV quoting rules and access fields by header name.

  2. 00:40

    That solves parsing, but it does not validate the business data. We will inspect what DictReader produces for missing and extra cells, convert text quantities to integers, reject blank names and nonpositive quantities, and test a reordered header. The website exercise runs from in-memory text with io.StringIO, so it does not need access to a visitor's files. A local script can use the same reader with a file opened in text mode and newline set appropriately. The result we want is not merely a parser that avoids an exception; it is a small importer whose output has a dependable shape and meaningful values.

  3. 01:20

    The hand split returns a list with three elements. The quote marks remain attached to fragments because str.split does not understand CSV syntax. DictReader sees the header name,qty, reads the quoted cell as one field, and produces a dictionary with name and qty. Its quantity is still the string '4'. That detail matters: a parser can preserve the structure of an input without deciding its domain type. Calling int later is a separate operation that may fail. It is also tempting to write code that always takes row zero as the name and row one as the quantity.

  4. 02:00

    DictReader lets us use the header labels instead, so the file can put qty first and name second without silently changing the meaning. This is the first boundary of a robust import: parse according to the file format. Do not manually remove quote characters or split on commas unless you intend to reimplement the full CSV grammar, including escaped quotes, delimiters inside fields, and line breaks inside quoted cells. In ordinary Python code, the standard csv module is the right tool. The first row has fewer cells than the header. DictReader supplies None for qty. If we call int on it without checking, the program fails with an error that says

  5. 02:43

    little about the original damaged record. The second row has an extra cell. By default, DictReader stores overflow fields under a None key. Code that only reads name and qty might quietly discard the extra value, even though it could indicate that a comma shifted the file's columns. For this exercise, we reject both shapes. We also reject a header that lacks either required field, because otherwise every row would be interpreted under the wrong schema. These are application decisions, not behaviors DictReader promises to enforce. A different importer might collect row errors and continue, but it should still

  6. 03:23

    make that policy explicit. Notice the difference between an empty string and a missing cell: an empty name can be a present but invalid value, whereas None can mean the cell was absent. Both should fail here, but for different reasons. Once the structure is checked, we strip surrounding whitespace, convert qty to int, and require a strictly positive quantity. A zero quantity might be meaningful in another inventory system; this lesson defines a received-stock import where it is invalid. First the function checks the header set. The two required names can appear in either order; additional named columns are

  7. 04:04

    allowed in this small contract, although a stricter importer could reject them. Then it inspects every row for overflow and missing required cells before touching their contents. We strip the name and quantity text, convert the quantity, and apply the positive-value rule. The return value is a list of tuples, each containing a normalized name and an integer. Downstream code no longer needs to guess whether qty is text. The example deliberately puts qty before name and includes a comma inside the quoted product. That proves we did not accidentally depend on a column position or a manual split.

  8. 04:45

    There is one subtle limit: this compact version relies on int to raise ValueError for nonnumeric text, which fits the task contract but does not add the line number to that exception. A production import could wrap conversion to include reader.line_num and the source filename. The primary lesson is the order: parse the CSV grammar, verify structural expectations, normalize text, convert types, and enforce domain constraints. Skipping a stage can make a happy-path demo look correct while allowing damaged data through. The first two assertions test successful imports with the usual header order and the reversed one.

  9. 05:25

    They also prove that surrounding whitespace is stripped and a quoted comma remains inside the name. The loop then feeds several deliberately bad inputs. One lacks the required qty header. One has a blank name. Another supplies text where an integer is needed. Zero violates the positive-quantity rule. A short row lacks a cell, and a long row has an extra cell. Each must raise ValueError; if any is accepted, the script raises an assertion instead of printing checks passed. That is much stronger evidence than a single example that happens to parse. Consider what an empty string should mean: this function rejects it because it has no

  10. 06:05

    required header. A header-only file, however, has a valid schema and returns an empty list. That difference is part of the input contract and worth testing in a real import pipeline. On the lesson page, start from the broken function that returns quantity strings, then add these checks. After the practical task passes, the quiz asks you to distinguish parsing from type conversion and schema validation. If you adapt the code to a local file, open it with newline='' as the csv documentation recommends, and keep the same validation order. Most importantly, do not assume that a successfully parsed row is automatically a

  11. 06:48

    trustworthy inventory record.

Understand the concept

The introductory CSV lesson shows structured text. A real import also needs validation. csv.DictReader maps each row to names from the header, so code need not assume that quantity is the second column. A quoted cell may contain a comma; splitting lines on commas by hand destroys that structure. StringIO supplies a text stream for this browser exercise without reading any files from a visitor's computer.

DictReader does not convert numbers. The cell '12' remains a string until the application calls int. Inspect the required headers before processing rows; a missing header should cause a clear ValueError rather than a KeyError much later. A short record can produce None for a missing cell. An overlong record may put additional cells under the None key. Both shapes deserve an explicit policy.

This inventory importer accepts required columns name and qty in either order. It strips surrounding whitespace, rejects a blank name, converts qty to an integer, and rejects zero or negative quantities. It also rejects extra fields, because silently ignoring a shifted column can accept corrupted data. For a bulk import, an error report might be preferable; here fail-fast validation makes each bad sample visible.

read_stock returns a list of normalized name and integer quantity tuples. The task tests a quoted comma, reordered headers, whitespace, missing headers, a nonnumeric quantity, and short and long records. This is not a complete CSV schema engine. It teaches the correct sequence: parse according to CSV grammar, check the schema, convert types, then use the validated values.

  • DictReader uses header names
  • Quoted commas require a CSV parser
  • Parsed cells remain strings
  • Validate header and row shape before conversion

See it step by step

Read the code, predict the output, then compare it with the result.

01. Keep a quoted comma in one field

PYTHON
import csv, io
rows = list(csv.DictReader(io.StringIO('name,qty\n"Pens, blue",4\n')))
print(rows)
EXPECTED OUTPUT
[{'name': 'Pens, blue', 'qty': '4'}]

The parser preserves the comma inside quoted text; the quantity remains text until converted.

02. Read the same schema in another order

PYTHON
import csv, io
row = next(csv.DictReader(io.StringIO('qty,name\n7,Paper\n')))
print(row['name'], int(row['qty']))
EXPECTED OUTPUT
Paper 7

Named access avoids a brittle positional assumption.

03. Inspect short and long rows

PYTHON
import csv, io
for text in ('name,qty\nPens\n', 'name,qty\nPens,2,extra\n'):
    print(next(csv.DictReader(io.StringIO(text))))
EXPECTED OUTPUT
{'name': 'Pens', 'qty': None}
{'name': 'Pens', 'qty': '2', None: ['extra']}

DictReader represents missing and overflow cells distinctly; both need validation.

A closer look

Follow the reasoning, inspect each result, then try the suggested changes in the console below.

01 / 03

Break a hand-written comma split

A comma does not always mark the end of a CSV field. Quoting allows a product name such as Pens, blue to remain one value. Splitting the line with str.split ignores those rules and produces three pieces where the import expects two. It may appear to work on simple rows, so reproduce the failure with a realistic quoted field first.

csv.DictReader reads the header and maps each parsed cell to its field name. It understands CSV quoting, but it deliberately leaves values as text. Compare the two outputs and note that the successful parser still returns the quantity '4' as a string. Parsing and type validation are separate responsibilities.

PYTHON
import csv, io
line = '"Pens, blue",4'
print(line.split(','))
print(next(csv.DictReader(io.StringIO('name,qty\n' + line + '\n'))))
EXPECTED OUTPUT
['"Pens', ' blue"', '4']
{'name': 'Pens, blue', 'qty': '4'}
Follow the reasoning
  1. Manual splitting ignores quoting.
  2. DictReader preserves the quoted comma.
  3. Quantity remains text until converted.
02 / 03

Inspect the shapes of damaged records

A valid header does not guarantee that every record has the same number of cells. DictReader fills a short row with None for its missing field. With too many cells, the overflow list is stored under a None key unless a different restkey was specified. Both outcomes are observable without crashing, which means silently accepting them is a real risk.

The lesson's importer rejects either shape before converting a quantity. Also check the header itself: if qty is absent, every row would be unusable. A clear ValueError at the beginning gives a better explanation than a later KeyError or an accidentally shifted value.

PYTHON
import csv, io
for text in ('name,qty\nPens\n', 'name,qty\nPens,2,extra\n'):
    row = next(csv.DictReader(io.StringIO(text)))
    print(row)
EXPECTED OUTPUT
{'name': 'Pens', 'qty': None}
{'name': 'Pens', 'qty': '2', None: ['extra']}
Follow the reasoning
  1. A missing cell is represented by None.
  2. An extra cell appears under a None key.
  3. Validation must reject both under this exercise's contract.
03 / 03

Normalize and validate an inventory import

Build a small importer whose return type is useful to the next operation: a list of name and integer-quantity tuples. The header set must include name and qty, but their order can change. Every row is checked for overflow and absent cells, and text is stripped before business validation. A name of only spaces is invalid after stripping.

Convert qty with int and require a strictly positive result. int raises ValueError for nonnumeric text, which matches this task's failure policy. The sample includes a quoted comma and a reordered header; both should work. The separate task checker adds malformed rows so a solution that only handles the happy path cannot pass.

PYTHON
import csv, io
def read_stock(csv_text):
    reader = csv.DictReader(io.StringIO(csv_text))
    if not {'name', 'qty'} <= set(reader.fieldnames or []):
        raise ValueError('name and qty headers required')
    result = []
    for row in reader:
        if None in row or not isinstance(row.get('name'), str) or not isinstance(row.get('qty'), str):
            raise ValueError('malformed row')
        name = row['name'].strip()
        qty = int(row['qty'].strip())
        if not name or qty <= 0:
            raise ValueError('invalid stock value')
        result.append((name, qty))
    return result
print(read_stock('qty,name\n4,"Pens, blue"\n'))
EXPECTED OUTPUT
[('Pens, blue', 4)]
Follow the reasoning
  1. Validate the schema before reading data rows.
  2. Normalize each field and convert quantity.
  3. Reject empty names, invalid quantities, and damaged row shapes.

Try it in Python

Edit the example and run it. Python starts in your browser the first time you click Run.

Python console

Ready to run
Need input()? Add one value per line
Ctrl / ⌘ + Enter to run
OUTPUT
Your output appears here.
Your turnComplete read_stock(csv_text). Require name and qty headers in either order; return (stripped_name, positive_integer_qty) tuples. Raise ValueError for a missing header, blank name, invalid quantity, missing cell, or extra cell. The prepared output must be [('Pens', 3), ('Paper', 2)].
Need a hint?

Check set(reader.fieldnames or []) before looping. Reject None in row, verify both cells are strings, strip them, call int on qty, and require qty > 0.

Complete the task and select Check task to verify your code.

    Quick quiz

    Three questions. You can change your answers and try again.

    01.Why prefer csv.DictReader to line.split(',')?

    02.What type is row['qty'] immediately after reading a normal CSV row?

    03.What should this task do with a missing qty header?

    Typical mistakes

    Everyone meets these errors. See what causes them and how to fix them.

    Splitting a CSV record manually

    COMMON MISTAKE
    name, qty = line.split(',')
    FIX
    reader = csv.DictReader(io.StringIO(csv_text))

    What happens: A quoted product such as "Pens, blue" becomes multiple fields.
    Use the parser to respect quoting and header order.

    Trusting every parsed row

    COMMON MISTAKE
    quantity = int(row['qty'])
    FIX
    if row.get('qty') is None or None in row:
        raise ValueError('malformed row')
    quantity = int(row['qty'])

    What happens: Missing or shifted cells fail late or are silently ignored.
    Check row shape before type conversion and domain rules.

    Browse this lesson’s problem fixes

    Sources and further reading