A previous release had provenance metadata, but this release does not. View details.
modify-csv
Apply a user-provided Python function to every cell in one CSV data row, every cell in one column, or one cell. The CSV's first record is treated as its header and is never modified. Row and column indexes are zero-based; columns can also be addressed by their exact header text.
Install
Requires Python 3.11 or newer. modify-csv is a command-line tool, so the simplest way to install it is in its own environment with pipx or uv:
pipx install modify-csv
# or
uv tool install modify-csv
To install it into the current Python environment instead:
python -m pip install modify-csv
Quick start
Given people.csv:
name,email
Ada Lovelace, ADA@EXAMPLE.COM
Grace Hopper,Grace@Example.com
Generate a script template:
modify-csv template normalize.py
Edit normalize.py so eval returns the cleaned-up value:
from typing import Any
def eval(content: Any, row: int, col: int) -> Any:
return content.strip().lower()
Apply it to the email column:
modify-csv apply people.csv clean.csv --script-file normalize.py --column email
clean.csv now contains:
name,email
Ada Lovelace,ada@example.com
Grace Hopper,grace@example.com
Script contract
The supplied script must define exactly this callable shape:
from typing import Any
def eval(content: Any, row: int, col: int) -> Any:
return content
Any is provided by Python's standard-library typing module. The function
name, parameter names, and three-argument shape are enforced.
content is the current CSV field (initially a string), while row and col
are zero-based numeric indexes into the data records and columns. The script is
ordinary trusted Python and executes with the same permissions as the command.
- Return value: whatever
evalreturns is written with Python's CSV writer.Nonebecomes an empty cell and any other value is written asstr(value). When targets are chained,contentis whatever the previous target returned, so it may not be a string. - Script lifetime: the script runs once, then
evalis called for each cell. Top-level variables keep their values between calls, which is useful for lookup tables or counters. Code underif __name__ == "__main__":does not run. - Errors: if
evalraises an exception, the run stops, nothing is written, and the command exits with status 1. - The name
eval: definingevalhides Python's built-ineval()inside your script. Usebuiltins.evalif you need the built-in.
Usage
modify-csv has two commands, apply and template. Each has its own help
page with only the options that belong to it:
modify-csv --help # the commands
modify-csv apply --help # targets, script options and the script contract
modify-csv template --help
Apply a script
Give the input and output files, exactly one of --script or
--script-file, and at least one target:
modify-csv apply INPUT OUTPUT (--script CODE | --script-file PATH) TARGET... [--force]
| Target | Transforms |
|---|---|
--row INDEX |
every cell in one data row |
--column COLUMN |
every data cell in one column |
--cell ROW COLUMN |
one cell |
Rows and columns are numbered from zero; data row 0 is the first record after
the header. A column is an index or the column's exact header text; a value
that is a whole number is read as an index. Existing output files are
protected; pass --force to replace one.
Apply a script file to a column selected by header:
modify-csv apply input.csv output.csv --script-file normalize.py --column email
Apply inline code to data row 2:
modify-csv apply input.csv output.csv \
--script 'def eval(content, row, col): return content.strip()' \
--row 2
Apply inline code to the cell at data row 0 and column 1:
modify-csv apply input.csv output.csv \
--script 'def eval(content, row, col): return int(content) * 2' \
--cell 0 1
Inline scripts or script files
--script takes the whole script as a single argument, which suits one-line
functions. In POSIX shells, wrap it in single quotes so characters such as $
and " reach Python unchanged. For anything longer, such as imports, helper
functions or state shared between cells, write a file (modify-csv template
gives you a starting point) and pass it with --script-file.
Chain targets
Targets can be repeated and mixed. The script runs over them in the order they are given:
modify-csv apply input.csv output.csv --script-file normalize.py \
--column email --column name --row 0 --cell 3 phone
- A cell covered by several targets is transformed once per target, and each
time it receives the result of the previous transformation. In the example
above, the
emailandnamecells of data row 0 are transformed twice. - Every target is checked against the CSV before any cell changes. If one is out of range or names a missing header, nothing is written.
Use from Python
transform_csv does the same work as apply. Each target is a tuple:
("row", INDEX, None), ("column", COLUMN, None) or ("cell", ROW, COLUMN),
where a column is an index or exact header text. Pass one target or a sequence
of them, and any callable that takes (content, row, col):
from pathlib import Path
from typing import Any
from modify_csv import Target, transform_csv
def normalize(content: Any, row: int, col: int) -> Any:
return content.strip()
targets: list[Target] = [
("column", "email", None),
("row", 0, None),
("cell", 3, "phone"),
]
transform_csv(Path("input.csv"), Path("output.csv"), targets, normalize)
The name and signature checks apply only to scripts loaded by the command line.
Pass overwrite=True to replace an existing output file. Problems are raised as
modify_csv.errors.ModifyCsvError.
Generate a script template
modify-csv template transform.py
Use --force to replace an existing template.
Behaviour and limits
- Encoding: input is read as UTF-8, with or without a byte-order mark (BOM). Output is always UTF-8 without a BOM, so a BOM in the input is not kept. Some versions of Excel need the BOM to recognise UTF-8.
- Format: only comma-delimited files with double-quote quoting are read.
Other delimiters are not detected; a semicolon-separated file, for example, is
read as a single column. Output uses Python's CSV defaults, quoting fields only
where needed and ending lines with
\r\n, so the output can differ from the input byte for byte even where no cell changed. - Shape: every data row must have as many fields as the header. Otherwise the command reports the first mismatched row and writes nothing.
- Columns by name: header text must match exactly, including case and
spaces. A header name that appears more than once is reported as ambiguous;
select that column by index. A header that is a whole number, such as
2024, can only be selected by index, because whole numbers are read as indexes. - Output: the result is written to a temporary file in the output folder
and then moved into place, so a failed run never leaves a partial file. Missing
output folders are created. To edit a file in place, give the same path as
INPUTandOUTPUTand pass--force. - Memory: the whole input file is loaded into memory.
- Exit status:
0on success,1when the input, script or output has a problem (reported asmodify-csv: error: ...), and2for command-line usage errors.
Breaking changes
Releases that change the command line or Python API in incompatible ways are listed, with migration steps, in BREAKING_CHANGES.md.
Contributing
Development setup, tests and conventions are in CONTRIBUTING.md.
License
Licensed under the
GNU Affero General Public License v3
(AGPL-3.0-only).
Metadata
Release files for modify-csv 2026.10.9
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Loss of provenance
These files do not have provenance metadata, despite files in a prior release including this data.
We recommend further investigation before installing these files. View details.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| modify_csv-2026.10.9.tar.gz | 23.4 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| modify_csv-2026.10.9-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 45.9 kB
Release files / modify_csv-2026.10.9.tar.gz
Loss of provenance
This file belongs to a release without provenance metadata, despite a prior release including this data.
We recommend further investigation before installing this file. View details.
| Download URL | modify_csv-2026.10.9.tar.gz |
|---|---|
| Size | 23.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
416a38ad2af7a86d300b6bb7d68c752b82c5b6b77799332c5da49437701e2451
|
|
BLAKE2b-256 checksum How to use checksums |
fee9e4271f8373f9997692a557f0a5863115c6bedffeccc17dfff6c8b3efa574
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Release files / modify_csv-2026.10.9-py3-none-any.whl
Loss of provenance
This file belongs to a release without provenance metadata, despite a prior release including this data.
We recommend further investigation before installing this file. View details.
| Download URL | modify_csv-2026.10.9-py3-none-any.whl |
|---|---|
| Size | 22.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
484f8fa47f8140dcb95e032e2ac5d3e797dbea1979ecaba99bb8034dc1e089e3
|
|
BLAKE2b-256 checksum How to use checksums |
2897ac85e414d02a5776d689b1d577a28e3964a3d50277f6e60186d46ed809bb
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Security for modify-csv 2026.10.9
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.
Loss of provenance
A previous release of these files had provenance metadata, but this release does not.
While this could reflect a routine change by the project maintainers, it may also indicate that the release files have been compromised.
Release comparison
- Removed 2026.10.4 2 files With provenance
- Added 2026.10.9 2 files Without provenance
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.