csvdoctor
Find out what is wrong with a CSV file, and fix it, in one command.
CSV has no standard. Every export is a small gamble: Excel writes ;-separated
Windows-1252 in many countries, databases dump UTF-16, web tools add invisible
byte-order marks, and somebody always sorts the spreadsheet and leaves an empty
column behind. csvdoctor tells you exactly what is wrong and writes a clean,
UTF-8, comma-separated copy, without touching your original.
- Zero dependencies. Pure Python standard library. Works anywhere Python 3.9+ does.
- Honest. It re-checks its own output and tells you what it could not fix.
- Safe by default. It never silently deletes rows or blanks values. Anything destructive is opt-in.
- Usable everywhere. CLI, Python library, JSON output for scripts, and a pre-commit hook for CI.
Install
pip install git+https://github.com/jasonkeanuchristy1-cyber/csvdoctor.git
# or, to keep it out of your project environments:
pipx install git+https://github.com/jasonkeanuchristy1-cyber/csvdoctor.git
Quick start
$ csvdoctor check examples/messy.csv
examples/messy.csv
Encoding ........ Windows-1252
Delimiter ....... semicolon
Line endings .... CRLF
Size ............ 9 data rows x 8 columns
Found 15 issues (3 errors, 10 warnings, 2 info)
ERROR RAGGED_ROWS Wrong number of fields in 1 row (the header has 8): 1 too short.
rows: 7
-> fixed by `csvdoctor fix`
ERROR EXTRA_DATA Real data beyond the last header column in 1 row. This usually means an
unquoted delimiter inside a value (e.g. a comma in 'Smith, John').
rows: 8
-> needs manual review
...
(The full report is abridged here; examples/messy.csv is a
real, deliberately broken file you can try it on.)
Now fix it:
$ csvdoctor fix examples/messy.csv --dedupe --normalize-nulls --neutralize-formulas
Fixed examples/messy.csv -> examples/messy.fixed.csv
- Re-encoded from Windows-1252 to UTF-8
- Changed delimiter from semicolon to comma
- Trimmed whitespace in 2 cells
- Repaired garbled text in 1 cell
- Removed 1 empty row
- Padded 1 short row with empty fields
- Dropped 1 empty column: "Notes"
- Emptied 1 placeholder value (N/A, #REF!, ...)
- Prefixed 1 formula-like cell with an apostrophe
- Renamed duplicate columns: "Email" -> "Email_2"
- Removed 1 duplicate row
Still needs attention (4):
(row numbers below refer to the fixed file)
ERROR EXTRA_DATA ...
WARNING EXCEL_SCI_NOTATION ...
WARNING MIXED_TYPES ...
WARNING INCONSISTENT_DATES ...
The four leftovers are things no tool can safely decide for you: a row with a stray
delimiter, IDs that Excel already rounded, a 12o typed instead of 120, and dates
that could be day-first or month-first. csvdoctor shows you where they are and
leaves the data alone.
What it checks
| Check | Severity | Resolved by |
|---|---|---|
EMPTY_FILE no data at all |
error | - |
RAGGED_ROWS rows with too few fields, or extra empty fields |
error | fix |
EXTRA_DATA real data past the last column (usually an unquoted delimiter) |
error | manual |
DUPLICATE_HEADERS same column name twice (most tools drop one silently) |
error | fix renames x_2 |
ENCODING not UTF-8 (Windows-1252, Latin-1, UTF-16/32) |
warning | fix |
LINE_ENDINGS mixed CRLF / LF / CR (ignores newlines inside quoted cells) |
warning | fix |
NUL_BYTES NUL characters in the file |
warning | fix |
EMPTY_ROWS blank lines and rows of empty cells |
warning | fix |
EMPTY_HEADERS columns without a name |
warning | fix names column_N |
EMPTY_COLUMNS columns with no data in any row |
warning | fix |
WHITESPACE leading/trailing spaces, including non-breaking spaces |
warning | fix |
INVISIBLE_CHARS zero-width spaces, word joiners, stray BOMs |
warning | fix |
MOJIBAKE text like Café from a past UTF-8 / Windows-1252 mix-up |
warning | fix |
DUPLICATE_ROWS exact repeated rows |
warning | --dedupe |
EXCEL_ERRORS #REF!, #DIV/0!, #N/A... exported as data |
warning | --normalize-nulls |
FORMULA_INJECTION cells starting with =, @, +cmd... that spreadsheets may execute |
warning | --neutralize-formulas |
EXCEL_SCI_NOTATION IDs/phone numbers Excel rounded to 1.23457E+11 |
warning | manual (the digits are gone) |
MIXED_TYPES a mostly numeric/date column containing text like 12o |
warning | manual |
INCONSISTENT_DATES several date formats in one column |
warning | manual (day/month order is ambiguous) |
BOM UTF-8 byte-order mark |
info | fix (--bom to keep one for Excel) |
DELIMITER not a comma |
info | fix |
NULL_TOKENS N/A, null, none, - standing in for "missing" |
info | --normalize-nulls |
"fix" means the default csvdoctor fix handles it. --flags are opt-in because they
change or remove data: for example NA is a real value in some data sets (Namibia's
country code), so blanking placeholders is never done unless you ask.
Command reference
csvdoctor check FILE [FILE ...] report problems, change nothing
csvdoctor fix FILE write a cleaned copy
Use - as the file name to read from standard input.
check
| Option | Meaning |
|---|---|
--format text|json |
human report (default) or machine-readable JSON |
--fail-on error|warning|info|never |
exit status 1 if an issue at least this severe exists (default error) |
--encoding NAME |
skip detection and read the file as this encoding |
--delimiter CHAR |
skip detection (, ; tab pipe or any single character) |
fix
| Option | Meaning |
|---|---|
-o, --output PATH |
where to write (- for stdout). Default: <name>.fixed.csv next to the input |
--in-place |
overwrite the input; a .bak copy of the original is kept |
--dedupe |
remove exact duplicate rows |
--normalize-nulls |
turn N/A, null, none, - and spreadsheet errors into empty cells |
--neutralize-formulas |
prefix formula-looking cells with ' to prevent CSV injection |
--out-delimiter CHAR |
delimiter to write (default ,) |
--bom |
write a UTF-8 BOM so Excel reads accented characters correctly |
--crlf |
write Windows line endings |
--encoding, --delimiter |
as for check |
Progress messages go to stderr, so csvdoctor fix data.csv -o - | other-tool is safe.
Exit codes
| Code | Meaning |
|---|---|
0 |
no issues at or above --fail-on (check), or fixed with no errors remaining (fix) |
1 |
issues found (check), or errors remain after fixing (fix) |
2 |
usage error or unreadable file |
Use it in CI and pre-commit
Fail a pull request when a committed CSV is broken:
# .github/workflows/csv.yml
- run: pip install git+https://github.com/jasonkeanuchristy1-cyber/csvdoctor.git
- run: csvdoctor check data/*.csv --fail-on error
As a pre-commit hook:
repos:
- repo: https://github.com/jasonkeanuchristy1-cyber/csvdoctor
rev: v0.1.0
hooks:
- id: csvdoctor
Machine-readable output for scripts and dashboards:
csvdoctor check export.csv --format json | jq '.files[0].summary'
Use it as a library
from csvdoctor import read_table, diagnose, repair
raw = open("export.csv", "rb").read()
report = diagnose(read_table(raw))
for issue in report.issues:
print(issue.severity, issue.code, issue.message)
fixed = repair(raw, dedupe=True) # -> Repaired(data=bytes, actions=[...], ...)
open("clean.csv", "wb").write(fixed.data)
print("\n".join(fixed.actions))
| Function | Purpose |
|---|---|
read_table(raw, encoding=None, delimiter=None) |
detect encoding/dialect and parse bytes into a Table |
diagnose(table) |
return a Report with a list of Issue objects (code, severity, message, rows, fix) |
fix_table(table, ...) |
return cleaned rows plus a log of what changed |
render_csv(rows, delimiter=",", bom=False, crlf=False) |
serialise rows to UTF-8 CSV bytes |
repair(raw, ...) |
all of the above in one call |
Design principles
- Never lose data silently. Dropping rows and blanking values are opt-in. Rows that cannot be repaired safely are left untouched and reported.
- Don't guess when it matters.
03/02/2024could be 3 February or 2 March, so csvdoctor reports mixed date formats instead of "fixing" them. - Verify, don't claim. After
fix, the result is re-parsed and re-checked, and what remains is shown to you. - Idempotent. Running
fixon its own output changes nothing. This is covered by randomised tests.
Limitations
- The whole file is loaded into memory. Files up to a few hundred MB are fine.
- Speed is roughly 50,000 rows per second for
checkon a typical 8-column file (pure Python, no dependencies). - Encoding and delimiter detection are heuristics. If it guesses wrong, pass
--encoding/--delimiter. Windows-1252 and Latin-1 cannot be told apart with certainty, and csvdoctor assumes Windows-1252 whenever the bytes allow it. - It does not validate data against a schema (for that, see Frictionless) and does not infer or convert types.
Contributing
Bug reports with a small sample file are the most valuable contribution: real-world messy CSVs are exactly what makes this tool better. See CONTRIBUTING.md for setup and how to add a new check.
License
Metadata
Release files for csvdoctor 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| csvdoctor-0.1.0.tar.gz | 34.9 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| csvdoctor-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 61.0 kB
Release files / csvdoctor-0.1.0.tar.gz
| Download URL | csvdoctor-0.1.0.tar.gz |
|---|---|
| Size | 34.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
bf68ee7c3dd3010371cd2cf1a1fbdcd49cfc0bbc3a8ff30e2f83b4cf65d039e3
|
|
BLAKE2b-256 checksum How to use checksums |
789fca86b3e3eea62608fad00923c0babf006752229241cc05a32a3d6764ae8c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 12, 2026.
Transparency logRelease files / csvdoctor-0.1.0-py3-none-any.whl
| Download URL | csvdoctor-0.1.0-py3-none-any.whl |
|---|---|
| Size | 26.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
36aa3ffb50fd7d75e882044a0cd1e691d43575fab665850b0e65aa746f7dedc5
|
|
BLAKE2b-256 checksum How to use checksums |
0b194bd52cfd4e614d3ff7781c7f365d120b4294c340d4e73616da6088b08452
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 12, 2026.
Transparency log