A CSV Import Brief with Real Edge Cases, Not Just a Happy Path

Drafted and reviewed using AI assistance.

On this page

To brief an agent on CSV import, define the accepted file format and the data rules separately. Then provide small files with exact accepted or rejected results. “Read this CSV” leaves encoding, duplicates, blank values and failure behavior to guesswork.

The fictional catalog importer below validates a batch of candidate records. It does not write to a database. Its nine small fixtures were run locally with Python 3.12.14; they demonstrate one chosen contract, not support for every CSV dialect.

Start with a field dictionary and a file boundary

This importer accepts UTF-8 without a byte-order mark, at most 65,536 bytes, and one to 100 data records. It uses comma delimiters, double-quoted fields and doubled double quotes inside quoted fields. Its header must be exactly:

sku,title,quantity,note
Field Accepted value Conversion or failure
sku One to 20 ASCII uppercase letters, digits or hyphens Keep exactly; reject duplicates within the batch
title One to 80 Python string characters Keep exactly; no whitespace trimming
quantity Canonical decimal integer from 1 to 99 Convert to integer; reject 01, -1 and 1.0
note Zero to 200 Python string characters Empty becomes None; otherwise keep exactly

These are application choices. In particular, rejecting a UTF-8 BOM is this example's policy, not a property of CSV in general. A real importer could deliberately accept it using a suitable decoder. Decide that behavior rather than discovering it through user failures.

Quoted and unquoted empty fields both become the same empty string under this parser, so this contract cannot use them to distinguish null from an intentionally empty note. If your application needs that distinction, choose an explicit representation.

The Python 3.12 CSV documentation describes configurable dialects, newline handling and parsing options. It does not define your SKU or duplicate policy. Here parsing uses csv.reader with an explicit dialect and strict mode; a separate validator checks field counts and values.

Give the agent a valid file that includes difficult syntax

This sample is invented catalog data:

sku,title,quantity,note
KIT-01,"Desk, small",2,"Said ""hello"""
KIT-02,Lamp,1,"Line one
Line two"

It contains two data records, although the second note spans two physical lines. The local exercise produced these candidate records:

[
  {"sku":"KIT-01","title":"Desk, small","quantity":2,"note":"Said \"hello\""},
  {"sku":"KIT-02","title":"Lamp","quantity":1,"note":"Line one\nLine two"}
]

Do not split input into lines and then split each line on commas. That would lose both the quoted comma and the embedded newline. The fixture files preserve their actual bytes, including CRLF record separators and the LF inside the second note.

Attach failures with an exact reason

The following results came from the local teaching validator. Every rejected file returned no candidate records.

Fixture Deliberate condition Actual result
01-quoted-fields.csv Comma, escaped quotes and embedded newline Accepted, two records
02-empty-note.csv Empty final field Accepted, note becomes None
03-duplicate.csv DUP-01 appears twice DUPLICATE_SKU at logical record 3
04-bad-quantity.csv Quantity is -1 INVALID_QUANTITY
05-extra-column.csv Five fields instead of four COLUMN_COUNT
06-unclosed-quote.csv A quoted field never closes MALFORMED_CSV
07-invalid-utf8.csv Contains byte 0xff INVALID_UTF8
08-bom.csv Starts with UTF-8 BOM bytes UNSUPPORTED_BOM
09-wrong-header.csv Header starts title,sku INVALID_HEADER

Record numbering includes the header as record 1. It counts logical CSV records, not physical lines. A parser-level failure may have no reliable record number; this example returns null rather than guessing.

For the duplicate fixture, the full result is:

{
  "accepted": false,
  "records": [],
  "errors": [{"record": 3, "code": "DUPLICATE_SKU"}]
}

That result is a validation rejection. It must not be presented as “two rows imported” or a database transaction rollback: no database was contacted.

Choose whole-batch rejection or partial acceptance

Decision Whole-batch rejection, chosen here Partial acceptance, an alternative
One invalid record Reject the whole batch Accept valid records and report rejected ones
User correction Fix the file and resubmit it Decide how to resubmit only failures without duplication
Result counts Accepted candidate count is zero on failure Separate accepted, rejected and skipped counts
Persistence Validate first; define transaction behavior separately Define per-record write failures and retry semantics
Duplicate policy Reject duplicates within this file Still needs an explicit duplicate and conflict policy

Partial acceptance can be useful, but silently discarding malformed rows is not a complete policy. It needs a reviewable error result and a retry strategy. Whole-batch validation is simpler for this exercise because the user can correct one file and try again.

Existing database conflicts are outside this validator. Before adding persistence, specify whether a known SKU is an error, an update or a separate operation, and how concurrent changes are handled. Passing these fixtures does not establish those behaviors.

Copy this import brief

Implement a candidate validator for the attached fictional catalog CSV.
Use Python 3.12 csv.reader; preserve the selected dialect explicitly.

File contract:
- Strict UTF-8, no BOM, at most 65,536 bytes.
- Comma delimiter, double-quote quoting, doubled quotes for escaping.
- Use newline="" when opening a text file or constructing StringIO.
- Header exactly sku,title,quantity,note; one to 100 data records.
- A blank extra record is invalid; do not silently skip it.

Data contract:
- sku matches [A-Z0-9-]{1,20}, unchanged and unique within this file.
- title has 1-80 Python string characters, unchanged.
- quantity matches [1-9][0-9]? and becomes an integer.
- note has at most 200 characters; empty maps to None.
- Do not trim, normalize case, infer a delimiter or coerce other values.

Failure contract:
- Any failure rejects the batch and returns records: [].
- Return stable error codes and logical record numbers when available.
- Header is logical record 1; parser/encoding errors may use null.
- Keep format errors separate from field-validation errors.

Deliver:
- The validator and the nine fixtures with their exact expected results.
- A record of which checks were actually run and the runtime version.
- Questions about existing-record conflicts rather than invented behavior.

Scope: candidate validation only. Do not add database writes, upserts,
uploads, background jobs or spreadsheet export behavior.

Use the fixture pack as a starting point, then add examples tied to your own contract. A file-size boundary or non-ASCII title deserves its own check if that is a requirement you intend to rely on. The nine executed fixtures are evidence for their listed cases, not exhaustive coverage of the importer.