Skip to content

csv: handle empty fields with skipinitialspace#8304

Merged
youknowone merged 2 commits into
RustPython:mainfrom
widehyo1:fix-csv-reader-space-handling
Jul 20, 2026
Merged

csv: handle empty fields with skipinitialspace#8304
youknowone merged 2 commits into
RustPython:mainfrom
widehyo1:fix-csv-reader-space-handling

Conversation

@widehyo1

@widehyo1 widehyo1 commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Summary

csv.reader(..., skipinitialspace=True) preprocessed each record by splitting
it on the delimiter and trimming both ends of every piece. This had two
problems:

  • a field containing only spaces could produce an invalid Rust slice and panic;
  • trailing spaces were removed even though only initial spaces should be ignored;
  • delimiters inside quoted fields were treated as field boundaries, so valid quoted spaces could be removed before csv-core parsed the record.

For example, the old preprocessing could change:

'a, "b, c", d'

before passing it to csv-core, removing the space inside "b, c".

The raw split-and-join preprocessing is now removed. A small per-item quote scanner classifies field starts, quote boundaries, doubled quotes, escapes, delimiters, and record terminators. The skipinitialspace preprocessing discards only actual initial-space events and copies every other source byte unchanged.

The same scanner is consumed by the custom quote-mode reader, avoiding separate implementations of the quote and escape transitions.

QUOTE_NOTNULL and QUOTE_STRINGS

The CPython test also covers QUOTE_NOTNULL and QUOTE_STRINGS. For these
modes, an empty unquoted field must become None, while a quoted empty field
must remain an empty string:

',""'  -> [None, '']

Both fields have empty decoded byte contents, so the conversion cannot be
decided from the final field buffer alone. The reader must retain whether each
field began with a quote.

The existing custom QUOTE_NONE reader is therefore extended into a shared
reader path for:

  • QUOTE_NONE with an escape character;
  • QUOTE_NOTNULL;
  • QUOTE_STRINGS.

Each parsed field carries its bytes and a was_quoted flag. The flag is used
only when QUOTE_NOTNULL or QUOTE_STRINGS converts an empty unquoted field
to None. This avoids separate, duplicated functions for the two modes while
preserving the existing QUOTE_NONE escape behavior.

Test_Csv.test_read_skipinitialspace is unmarked now that its regular,
QUOTE_NOTNULL, and QUOTE_STRINGS cases pass.

Scope

This is intentionally a narrow reader fix. It does not introduce a complete
CPython-style CSV state machine and does not change:

  • strict parsing;
  • records spanning multiple iterator items;
  • multiline reader lifecycle;
  • complete QUOTE_STRINGS numeric conversion;
  • writer quoting or lineterminator handling.

Testing

temp/workspace/before-commit-csv.sh passed, including:

  • cargo fmt --check

  • cargo run --release Lib/test/test_csv.py

    • 128 tests passed
    • 6 skipped
    • 21 expected failures
  • cargo run -- extra_tests/snippets/stdlib_csv.py

  • cargo clippy -p rustpython-stdlib --all-targets

  • the configured workspace test suite

related issue

Summary by CodeRabbit

Summary by CodeRabbit

  • Bug Fixes
    • Improved CSV reader behavior for quoted fields, including correct handling of escaped quotes, delimiters, and record/line termination.
    • When skipinitialspace=True, initial-space trimming is now quote-aware, preserving spaces inside quoted fields.
    • Empty unquoted fields and empty quoted fields are now handled distinctly (null vs empty string, as applicable).
    • Refined parsing behavior when escapechar is configured.
  • Tests
    • Added a regression test for skipinitialspace=True ensuring spaces inside quoted fields are preserved.

@coderabbitai

coderabbitai Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: 9687b0e8-15de-48e5-9d92-dc0b6df6e208

📥 Commits

Reviewing files that changed from the base of the PR and between 352ecf7 and 0d784ab.

⛔ Files ignored due to path filters (1)
  • Lib/test/test_csv.py is excluded by !Lib/**
📒 Files selected for processing (2)
  • crates/stdlib/src/csv.rs
  • extra_tests/snippets/stdlib_csv.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • extra_tests/snippets/stdlib_csv.py
  • crates/stdlib/src/csv.rs

📝 Walkthrough

Walkthrough

The CSV reader replaces its unquoted parser with a shared quote-aware parser, preserves quote provenance during field conversion, updates parser routing and initial-space trimming, and adds a regression test for spaces inside quoted fields.

Changes

CSV quote-aware parsing

Layer / File(s) Summary
Quote-aware record parsing
crates/stdlib/src/csv.rs
Adds stateful scanning for quotes, escapes, delimiters, and record terminators; enforces field limits, decodes UTF-8, preserves quoted empty strings, and maps empty unquoted fields to None for applicable quote styles.
Reader routing and space trimming
crates/stdlib/src/csv.rs, extra_tests/snippets/stdlib_csv.py
Routes supported modes through the shared parser, trims only leading spaces in unquoted fields, and tests preservation of spaces inside quoted fields.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related PRs

Suggested reviewers: youknowone, shaharnaveh

Sequence Diagram(s)

sequenceDiagram
  participant Reader
  participant RecordParser
  participant FieldConversion
  Reader->>RecordParser: select quote-aware parsing mode
  RecordParser->>RecordParser: scan quotes, escapes, delimiters, and terminators
  RecordParser->>FieldConversion: provide decoded fields with quote provenance
  FieldConversion-->>Reader: return converted CSV row
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and accurately reflects the main CSV reader fix around skipinitialspace and empty fields.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

📦 Library Dependencies

The following Lib/ modules were modified. Here are their dependencies:

[x] lib: cpython/Lib/csv.py
[x] test: cpython/Lib/test/test_csv.py (TODO: 19)

dependencies:

  • csv

dependent tests: (4 tests)

  • csv: test_csv test_genericalias
    • importlib.metadata: test_importlib test_zoneinfo

Legend:

  • [+] path exists in CPython
  • [x] up-to-date, [ ] outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/stdlib/src/csv.rs`:
- Around line 1164-1170: Update the skipinitialspace preprocessing in the CSV
parsing path so it does not split or trim delimiter-separated segments inside
quoted fields. Make the trimming logic quote- and escape-aware, or move it into
the parser, preserving spaces within quoted values while still removing initial
spaces from unquoted fields.
- Around line 1086-1100: Update the field conversion closure in the CSV parsing
path to handle QuoteStyle::Strings distinctly: preserve quoted fields as
strings, return None for empty unquoted fields, and parse nonempty unquoted
fields as floats. Use the existing VM float-construction/parsing utilities and
retain current UTF-8 error handling for values that remain strings.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: 6daadf40-d52b-4fa5-b1b1-353e391a14de

📥 Commits

Reviewing files that changed from the base of the PR and between 1205fd2 and 51a96d5.

⛔ Files ignored due to path filters (1)
  • Lib/test/test_csv.py is excluded by !Lib/**
📒 Files selected for processing (1)
  • crates/stdlib/src/csv.rs

Comment thread crates/stdlib/src/csv.rs
Comment thread crates/stdlib/src/csv.rs Outdated
assert list(reader) == [["a", "b, c", "d"]]


test_reader_skipinitialspace_preserves_quoted_spaces()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isnt this test covered by test_read_skipinitialspace in test_csv? in that case, we don't need this test. could you check if it is that case or not?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

test_read_skipinitialspace only covers unquoted input. Its QUOTE_NOTNULL and QUOTE_STRINGS cases contain blank fields, while the other cases use the default QUOTE_MINIMAL mode.
It does not cover the following case, where a quoted field contains a delimiter followed by a space:

reader = csv.reader(['a, "b, c", d'], skipinitialspace=True)

The implementation in 51a96d5 trimmed the input without respecting quote boundaries, causing the CPython incompatibility described in this review comment. so I added this regression case to extra_tests.

@widehyo1
widehyo1 requested a review from youknowone July 19, 2026 02:29
widehyo1 added 2 commits July 19, 2026 14:00
skipinitialspace preprocessed records by trimming both ends of every split
field. Fields containing only spaces could therefore produce an invalid
slice, while trailing spaces were removed even though CPython preserves them.

Trim only field prefixes in the csv-core path and make the remaining
all-space trimming helper return an empty slice safely.

QUOTE_NOTNULL and QUOTE_STRINGS add another distinction: an unquoted empty
field becomes None, but a quoted empty field remains an empty string. Both
forms have identical decoded bytes, so extend the existing QUOTE_NONE custom
reader into one shared path that records whether each field started quoted.
Use that metadata only for the null conversion while retaining QUOTE_NONE
escape behavior.

Unmark Test_Csv.test_read_skipinitialspace now that its standard,
QUOTE_NOTNULL, and QUOTE_STRINGS cases pass.

This keeps the existing one-item reader lifecycle and does not add the larger
strict or multiline parser state machine.

Assisted-by: Codex:gpt-5-sol
The csv-core preprocessing path split each raw iterator item on every
delimiter before trimming field prefixes. Delimiters inside quoted fields were
therefore treated as separators, so spaces after them could be removed before
csv-core parsed the record.

Extract the quote, escape, delimiter, and field-start transitions from
read_quote_record() into a small per-item scanner. Reuse its events in the
skipinitialspace preprocessor and discard only InitialSpace events while
copying all other source bytes unchanged.

This keeps the custom quote-mode reader and preprocessing logic aligned
without introducing persistent multiline or strict parser state. Remove the
delimiter cache that was only needed by the old split-and-join path and add a
regression for spaces following a delimiter inside a quoted field.

Assisted-by: Codex:gpt-5-sol
@youknowone
youknowone force-pushed the fix-csv-reader-space-handling branch from 352ecf7 to 0d784ab Compare July 19, 2026 05:02

@youknowone youknowone left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

@youknowone
youknowone enabled auto-merge (squash) July 19, 2026 05:03
@youknowone
youknowone merged commit d16fd12 into RustPython:main Jul 20, 2026
50 of 51 checks passed
@widehyo1
widehyo1 deleted the fix-csv-reader-space-handling branch July 20, 2026 11:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

z-ca-2026 Tag to track Contribution Academy 2026

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants