Skip to content

fix(dataset): keep CSV export columns aligned - #2079

Open
anxkhn wants to merge 5 commits into
apify:masterfrom
anxkhn:fix/csv-export-column-alignment
Open

fix(dataset): keep CSV export columns aligned#2079
anxkhn wants to merge 5 commits into
apify:masterfrom
anxkhn:fix/csv-export-column-alignment

Conversation

@anxkhn

@anxkhn anxkhn commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

export_csv_to_stream (src/crawlee/_utils/file.py) built the CSV header from the
first non-empty item's keys, but then wrote every row positionally with
csv.writer and item.values(). Dataset.push_data accepts an arbitrary
Mapping[str, JsonSerializable] per item with no schema enforcement, so a dataset
where later items have a different key set or key order than the first (normal when
different handlers push different fields) had its values written under the wrong
columns, or shifted into columns that do not exist, silently corrupting the CSV.

Example: pushing

{'name': 'Alice', 'age': 30}
{'name': 'Bob', 'city': 'NYC', 'age': 25}
{'age': 40, 'name': 'Carol'}

and exporting with content_type='csv' produced

name,age
Alice,30
Bob,NYC,25
40,Carol

Bob's row has three values in two columns, and Carol's row puts age (40) under
name and name (Carol) under age.

This change writes rows with csv.DictWriter, keyed on the first item's fields and
extrasaction='ignore', so each value lands in its own column and keys not in the
header are dropped. The same input now exports as

name,age
Alice,30
Bob,25
Carol,40

which matches the column-aligned behavior of Crawlee for JavaScript (whose CSV
export, by default, derives the header from the first item, maps each row by key,
and ignores extra keys). Items missing a header key get an empty cell, as before for
absent values.

The existing lineterminator='\n' default (which avoids double line endings on
Windows) is preserved: it still flows through **kwargs into DictWriter.

Issues

  • No existing issue; this is a self-reported bug found while reading the dataset
    export path. (Happy to open a tracking issue first if the maintainers prefer.)

Testing

  • Added two regression tests in tests/unit/_utils/test_file.py:
    • test_export_csv_to_stream_keeps_columns_aligned_for_heterogeneous_items -
      asserts values stay under their own header column for items with differing key
      sets/orders (fails on the old positional code with the exact misalignment above).
    • test_export_csv_to_stream_skips_empty_items - empty mappings are skipped and do
      not define or shift the header.
  • uv run pytest tests/unit/_utils/test_file.py tests/unit/storages/test_dataset.py
    plus the test_basic_crawler.py push-and-export tests: 273 passed, 2 skipped
    (unrelated memory-storage persistence skips).
  • uv run poe lint (ruff format + check) and uv run poe type-check (ty) are clean
    for the changed files.

Checklist

  • CI passed

@Mantisus Mantisus left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thank you for finding this bug. However, we cannot use extrasaction=‘ignore’

Comment thread src/crawlee/_utils/file.py Outdated
Comment thread tests/unit/_utils/test_file.py

Copilot AI 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.

Pull request overview

This PR fixes CSV export for Dataset items that don’t share identical key order/sets, preventing values from being written under the wrong column when exporting heterogeneous items.

Changes:

  • Update export_csv_to_stream to write rows by key (via csv.DictWriter) instead of positional item.values().
  • Add regression tests covering heterogeneous item keys/order and skipping empty items during header selection.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
src/crawlee/_utils/file.py Switches CSV export implementation to key-based writing and changes how CSV headers are derived.
tests/unit/_utils/test_file.py Adds regression tests for column alignment with heterogeneous items and for skipping empty mappings.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/crawlee/_utils/file.py Outdated
Comment thread src/crawlee/_utils/file.py Outdated
Comment thread src/crawlee/_utils/file.py Outdated
Comment thread src/crawlee/_utils/file.py Outdated
anxkhn added 3 commits July 27, 2026 18:26
`export_csv_to_stream` built the header from the first item's keys but then
wrote every row positionally with `csv.writer` and `item.values()`. Since
`Dataset.push_data` accepts arbitrary mappings per item with no schema, items
whose keys differ in membership or order from the first item had their values
written under the wrong columns (or shifted into columns that do not exist),
silently corrupting the CSV export of any non-uniform dataset.

Write rows with `csv.DictWriter` keyed on the first item's fields and
`extrasaction='ignore'`, so each value lands in its own column and unexpected
keys are dropped. This matches the column-aligned behavior of Crawlee for
JavaScript.

Signed-off-by: Anas Khan <83116240+anxkhn@users.noreply.github.com>
Signed-off-by: Anas Khan <83116240+anxkhn@users.noreply.github.com>
Signed-off-by: Anas Khan <83116240+anxkhn@users.noreply.github.com>
@anxkhn
anxkhn force-pushed the fix/csv-export-column-alignment branch from 5b95959 to bde4ba2 Compare July 27, 2026 12:56
@anxkhn anxkhn changed the title fix(dataset): keep CSV columns aligned for items with different keys fix(dataset): bound CSV export memory usage Jul 27, 2026
@anxkhn

anxkhn commented Jul 27, 2026

Copy link
Copy Markdown
Contributor Author

pushed a revision addressing the latest review and ci feedback.

@vdusek

vdusek commented Jul 27, 2026

Copy link
Copy Markdown
Collaborator

Same as here #2074 (comment)

@anxkhn Thank you. For future reviews, I'd really appreciate it if you could address all the feedback in a single follow-up commit, rather than splitting the changes across multiple commits, merging them into earlier commits, and force-pushing it. Your current approach makes the changes really hard to review.

@vdusek vdusek left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

A few important things and then a few nits.

Comment thread src/crawlee/_utils/file.py Outdated
Comment thread src/crawlee/_utils/file.py Outdated
Comment thread src/crawlee/_utils/file.py Outdated
Comment thread src/crawlee/_utils/file.py Outdated
Comment thread src/crawlee/_utils/file.py
Comment thread tests/unit/_utils/test_file.py Outdated
Comment thread tests/unit/_utils/test_file.py
Signed-off-by: Anas Khan <83116240+anxkhn@users.noreply.github.com>
@anxkhn

anxkhn commented Jul 27, 2026

Copy link
Copy Markdown
Contributor Author

@vdusek addressed all new threads, including js-default parity, streaming behavior, bounded default memory, and value preservation tests. rebased and re-requesting review. thank you.

…eagerly

Address review findings on the CSV export rewrite:

- Warn once per export, naming every key dropped because it is absent from the first item, instead
  of losing those values silently. Master wrote them to the wrong column, which was wrong but at
  least visible.
- Build a throwaway writer up front so invalid options (e.g. `delimiter='ab'`) raise even when the
  dataset is empty. Master validated eagerly; the lazy writer made a misconfigured export succeed
  on a run that scraped nothing and fail on the next one.
- Pass `extrasaction='ignore'` on the buffered path too, so both paths construct the writer
  identically and narrowing `fieldnames` later cannot raise mid-export.
- Declare `restval`, which already worked but was rejected by the type checker at every call site,
  and correct the `ExportDataCsvWriterKwargs` docstring that named `csv.DictWriter` while listing
  only `csv.writer` options.
- Cover the empty-items guard, the `if item` filter on the buffered path, and `collect_all_keys`
  through `Dataset.export_to` and `BasicCrawler.export_data`, none of which had a test.
- Document the column behavior and `collect_all_keys` in the dataset export example.
@vdusek vdusek changed the title fix(dataset): bound CSV export memory usage fix(dataset): keep CSV export columns aligned Jul 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants