CSV++ reference implementation — arrays and structs in CSV headers, backward-compatible with every CSV tool
Project description
csvpp — CSV++ Reference Implementation
Python reference implementation of draft-mscaldas-csvpp-02, the IETF Internet-Draft extending RFC 4180 CSV with support for array fields and structured/nested fields.
Quick Start
import csvpp
# Array fields
records = csvpp.parse("""id,name,phone[|],email[;]
1,John,555-1234|555-5678|555-9012,john@work.com;john@home.com
2,Jane,555-4444,jane@company.com
""")
records[0]["phone"] # ["555-1234", "555-5678", "555-9012"]
records[0]["email"] # ["john@work.com", "john@home.com"]
# Structured fields
records = csvpp.parse("""id,name,geo^(lat^lon)
1,Location A,34.0522^-118.2437
""")
records[0]["geo"] # {"lat": "34.0522", "lon": "-118.2437"}
# Array of structures
records = csvpp.parse("""id,name,address[~]^(street^city^state^zip)
1,John,123 Main St^Los Angeles^CA^90210~456 Oak Ave^New York^NY^10001
""")
records[0]["address"][0] # {"street": "123 Main St", "city": "Los Angeles", ...}
# Parse from file
records = csvpp.parse_file("examples/appendixB_ecommerce.csvpp")
Installation
The library has no runtime dependencies — it uses only the Python standard library.
Test tooling (pytest, pytest-cov) is listed in requirements.txt.
# 1. Create and activate a virtual environment
python -m venv .venv
source .venv/bin/activate # macOS / Linux
# .venv\Scripts\activate # Windows
# 2a. Install with pip (editable, includes test deps)
pip install -e ".[dev]"
# 2b. Or install test deps directly from requirements.txt
pip install -r requirements.txt
pip install -e .
Running Tests
# Run all 88 tests
pytest
# Run with coverage report
pytest --cov=csvpp --cov-report=term-missing
# Run a specific test file
pytest tests/test_examples.py
# Run a specific test class or case
pytest tests/test_examples.py::TestAppendixBEcommerce
pytest tests/test_examples.py::TestAppendixBEcommerce::test_shirt
Using the Library
Parsing a string
import csvpp
# Simple fields — values are plain strings
records = csvpp.parse("id,name\n1,Alice\n2,Bob\n")
# [{"id": "1", "name": "Alice"}, {"id": "2", "name": "Bob"}]
# Array fields — values are lists
records = csvpp.parse("""id,name,phone[|],email[;]
1,John,555-1234|555-5678|555-9012,john@work.com;john@home.com
2,Jane,555-4444,jane@company.com
""")
records[0]["phone"] # ["555-1234", "555-5678", "555-9012"]
records[1]["email"] # ["jane@company.com"]
# Structured fields — values are dicts
records = csvpp.parse("""id,name,geo^(lat^lon)
1,Location A,34.0522^-118.2437
""")
records[0]["geo"] # {"lat": "34.0522", "lon": "-118.2437"}
# Array of structures
records = csvpp.parse("""id,name,address[~]^(street^city^state^zip)
1,John,123 Main St^Los Angeles^CA^90210~456 Oak Ave^New York^NY^10001
""")
records[0]["address"][0] # {"street": "123 Main St", "city": "Los Angeles", "state": "CA", "zip": "90210"}
records[0]["address"][1] # {"street": "456 Oak Ave", "city": "New York", "state": "NY", "zip": "10001"}
# Complex nesting (array within struct, struct within struct)
records = csvpp.parse("""id,cust,items[~]^(sku^name^qty^price^opts[;]:(k:v))
1,Alice,S1^Shirt^2^20^sz:M;col:blu~S2^Pant^1^50^sz:32
""")
records[0]["items"][0]
# {"sku": "S1", "name": "Shirt", "qty": "2", "price": "20",
# "opts": [{"k": "sz", "v": "M"}, {"k": "col", "v": "blu"}]}
Parsing a file
import csvpp
records = csvpp.parse_file("examples/appendixB_ecommerce.csvpp")
Pretty-printing records
csvpp.pprint() renders any parsed result with aligned keys, box borders, and
recursive indentation for arrays and nested structs. Use top=N to cap the
number of rows displayed.
import csvpp
records = csvpp.parse_file("examples/figure5_repeated_structs.csvpp")
csvpp.pprint(records) # all records, ANSI colour if terminal
csvpp.pprint(records, top=1) # first record only
csvpp.pprint(records, color=False) # plain text (no ANSI escape codes)
Example output — figure5_repeated_structs.csvpp (array of structs):
┌ Record 1/2 ────────────────────────────────────────────────┐
│ id 1
│ name John
│ address [0] street 123 Main St
│ city Los Angeles
│ state CA
│ zip 90210
│ [1] street 456 Oak Ave
│ city New York
│ state NY
│ zip 10001
└────────────────────────────────────────────────────────────┘
┌ Record 2/2 ────────────────────────────────────────────────┐
│ id 2
│ name Jane
│ address [0] street 789 Pine St
│ city Boston
│ state MA
│ zip 02101
└────────────────────────────────────────────────────────────┘
Example output — appendixB_ecommerce.csvpp (4-level nesting):
┌ Record 1/1 ────────────────────────────────────────────────┐
│ id 1
│ cust Alice
│ items [0] sku S1
│ name Shirt
│ qty 2
│ price 20
│ opts [0] k sz
│ v M
│ [1] k col
│ v blu
│ [1] sku S2
│ name Pant
│ qty 1
│ price 50
│ opts [0] k sz
│ v 32
└────────────────────────────────────────────────────────────┘
top=N — shows N records and prints an omission note:
┌ Record 1/5 ───...
...
┌ Record 2/5 ───...
...
Showing 2 of 5 records (3 omitted). Pass top=5 to see all.
| Parameter | Type | Default | Description |
|---|---|---|---|
records |
list[dict] |
— | Output of parse() / parse_file() |
top |
int | None |
None |
Max rows to display; None = all |
file |
TextIO |
sys.stdout |
Output stream |
color |
bool | None |
None |
ANSI colour: True/False/auto-detect |
Working with field schemas directly
from csvpp import parse_field, parse_header_row
# Parse a single header
field = parse_field("address[~]^(street^city^state^zip)")
# ArrayField('address', delimiter='~',
# element_type=StructField('address', component_delimiter='^',
# components=[SimpleField('street'), SimpleField('city'), ...]))
# Parse a full header row
schemas = parse_header_row(["id", "phone[|]", "address[~]^(street^city)"])
Error handling
import csvpp
try:
records = csvpp.parse(my_csv_text)
except csvpp.HeaderParseError as e:
print(f"Bad column header: {e}")
except csvpp.DelimiterConflictError as e:
print(f"Reused delimiter: {e}")
except csvpp.InvalidQuotingError as e:
print(f"Non-leaf element was quoted: {e}")
except csvpp.CSVPPError as e:
print(f"Other CSV++ error: {e}")
Project Structure
ri/
├── src/csvpp/
│ ├── __init__.py — public API (parse, parse_file, + model types)
│ ├── models.py — field type data models + exceptions
│ ├── header_parser.py — parse CSV++ column header declarations
│ ├── value_parser.py — parse data values per header schema
│ └── parser.py — main two-phase parser (orchestration)
├── tests/
│ ├── test_header_parser.py — Phase 1: header parsing
│ ├── test_arrays.py — Phase 2: array value parsing
│ ├── test_structures.py — Phase 3: struct value parsing
│ ├── test_nested.py — Phase 5: nested structures
│ ├── test_quoting.py — Phase 6: RFC 4180 leaf quoting
│ ├── test_parser.py — Phase 7: full parser integration
│ ├── test_examples.py — all spec figures (Figs 1–7 + Appendix B)
│ └── test_validation.py — Phase 8: error handling & validation
└── examples/ — sample .csvpp files from the spec
CSV++ Syntax Summary
| Feature | Header syntax | Example value |
|---|---|---|
| Simple field | name |
Alice |
| Array (explicit delim) | `phone[ | ]` |
Array (default ~) |
phone[] |
555-1234~555-5678 |
| Struct (explicit delim) | geo^(lat^lon) |
34.05^-118.24 |
Struct (default ^) |
address(street^city) |
Main St^Boston |
| Array of structs | address[~]^(street^city^state^zip) |
Main^LA^CA^90210~Oak^NY^NY^10001 |
| Nested (array in struct) | address[~]^(type^lines[;]^city) |
home^Main;Apt 4^LA |
| Nested (struct in struct) | loc^(name^coords:(lat:lon)) |
Office^34.05:-118.24 |
Return Value Structure
parse() / parse_file() return list[dict]. Values in each dict:
| Field type | Python value |
|---|---|
SimpleField |
str |
ArrayField[simple] |
list[str] |
ArrayField[struct] |
list[dict] |
StructField |
`dict[str, str |
Spec Compliance
- Header row is required (no headerless files)
- Default array delimiter:
~(top-level arrays only; nested arrays must specify explicit delimiter) - Default component delimiter:
^ - RFC 4180 quoting applies only to leaf elements (innermost atomic values); quoting non-leaf elements raises
InvalidQuotingError - Same delimiter at multiple nesting levels raises
DelimiterConflictError - Nesting depth >4 issues
NestingDepthWarning
Exceptions
| Exception | When raised |
|---|---|
HeaderParseError |
Malformed column header |
DelimiterConflictError |
Same delimiter reused at multiple nesting levels |
InvalidQuotingError |
RFC 4180 quoting applied to a non-leaf element |
ValueParseError |
Value cannot be parsed per its schema |
NestingDepthWarning |
Nesting depth exceeds recommended 3–4 levels |
All inherit from csvpp.CSVPPError.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file csvplusplus-0.1.0.tar.gz.
File metadata
- Download URL: csvplusplus-0.1.0.tar.gz
- Upload date:
- Size: 20.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.10
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7e64d2bbdff6cb53df5b6cd6b03d7e316545832c05bcc3cf522d42f23642212d
|
|
| MD5 |
99acf13f80330d05778c322040cc8949
|
|
| BLAKE2b-256 |
530104db6068edb2a10f60f6e5c4add5707023abafcf71870469864bf51db3f1
|
File details
Details for the file csvplusplus-0.1.0-py3-none-any.whl.
File metadata
- Download URL: csvplusplus-0.1.0-py3-none-any.whl
- Upload date:
- Size: 15.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.10
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8edbb07a2546d419baf4e4c0698f07ca1b1b06d0f29651ef348df89ddcca7080
|
|
| MD5 |
9fd53cc1a1c8a7b0411cda86dccbbf00
|
|
| BLAKE2b-256 |
89bb0ace01ee6ac65e463271ae79b0f04c4cacad1acb8eb2d2e0ea10c5e42d8b
|