Convert & export data · 4 min read

Convert TSV to CSV in Python Without Changing Field Values

Replacing every tab with a comma works only for very simple text. A tab inside a quoted value belongs to the data, not to the column structure.

Identify the TSV dialect before converting it

For this example, a literal tab separates fields, double quotes enclose a field containing special characters, and doubled quotes represent a quote inside that field. Those are the rules the script uses. The sample includes a comma, quotation marks, an embedded tab, a blank final field and an embedded newline.

Some systems produce tab-separated text without any quoting mechanism, or use backslash escape sequences such as a literal backslash followed by t. That is a different input contract. Check the exporter documentation or a small known record before choosing parsing options.

Character in the sampleMeaningConversion requirement
Tab outside quotesField separatorBecomes a comma separator
Tab inside quotesPart of the noteRemain inside that field value
Doubled quote inside a quoted fieldOne quote characterPreserve through CSV quoting
Newline inside quotesPart of the noteRemain inside one logical record
Quoted TSV value: "left<TAB>right"; One note field. Parsed value: left<TAB>right; Still one note field. TAB labels a literal tab in this synthetic illustration; it is not text added to the output.
Illustrated example. TAB labels a literal tab in this synthetic illustration; it is not text added to the output.

Save the example with real tab characters

Save the primary sample as input.tsv in a working folder. The sample contains real tabs and one newline inside a quoted note. A text editor may show whitespace markers, which can help distinguish those characters from spaces.

Save the code below as tsv_to_csv.py. It requires Python 3 and no extra package. Run python tsv_to_csv.py input.tsv output.csv. Choose an unused output filename, since the script will not overwrite an existing destination.

tsv_to_csv.py — complete scriptDownload Python file ↓
import csv, sys

if len(sys.argv) != 3:
    raise SystemExit("Usage: python tsv_to_csv.py input.tsv output.csv")
with open(sys.argv[1], encoding="utf-8-sig", newline="") as f:
    reader = csv.reader(f, delimiter="\t", quotechar='"', strict=True)
    header = next(reader, None)
    if not header or any(not h.strip() for h in header):
        raise ValueError("Nonempty headers required")
    if len(header) != len(set(header)):
        raise ValueError("Duplicate headers require a schema decision")
    rows = []
    for number, row in enumerate(reader, 2):
        if len(row) != len(header):
            raise ValueError(f"Record {number}: wrong number of fields")
        rows.append(row)
with open(sys.argv[2], "x", encoding="utf-8", newline="") as f:
    writer = csv.writer(f, lineterminator="\r\n")
    writer.writerow(header)
    writer.writerows(rows)
with open(sys.argv[2], encoding="utf-8", newline="") as f:
    round_trip = list(csv.reader(f, strict=True))
if round_trip != [header, *rows]:
    raise ValueError("Converted CSV does not preserve the input fields")
print(f"Wrote {len(rows)} data records; field values verified")

Inspect fields rather than the visual line count

The expected output has three data records, even though the third note occupies two physical lines in a text editor. The second record still has three fields because its final status is empty. Removing a trailing separator would change that record shape.

A comma in the first note requires quoting in CSV; the writer handles it. The tab in the second note is retained as data, because comma is now the separator. Do not replace that surviving tab merely to make the output look uniformly comma-separated.

Expected CSV field content
id,note,status
00123,"Blue, ""large"" mug",ready
00456,left	right,
00789,"line one
line two",hold

Related help: Understand why physical lines and CSV records differ

Check a round trip before using your full file

The converter reads its output back with a CSV parser and compares every cell string with the TSV parser result. It stops with an error if they differ; a successful run reports field values verified. This compares data rather than file bytes, because separators, outer quotes and record endings legitimately change during conversion.

Verify the three IDs, the empty status and the two embedded whitespace characters explicitly. If your real data has accented names or non-Latin scripts, include one known example in the comparison. A successful file write by itself does not establish that the chosen input dialect was correct.

TSV parsing: 3 data records; 3 fields per record; One embedded newline. CSV parsing: 3 data records; 3 fields per record; Same note strings. The executed synthetic round trip compared field values after conversion.
Illustrated example. The executed synthetic round trip compared field values after conversion.

Understand why the script can refuse a file

Blank or duplicate headers require a schema decision, so the script rejects them. It also rejects data records whose field count differs from the header and asks the parser to report recognized quoting errors. An empty physical line is not silently treated as a complete blank record.

The file must decode as UTF-8, optionally with a byte-order mark. A decoding error is a reason to determine the actual source encoding, not to replace undecodable bytes with question marks. This converter is not an encoding detector or a universal TSV validator.

Keep destination interpretation separate from conversion

The converter preserves 00123 as a text field in the CSV. A spreadsheet can still interpret it as a number when opening the result. Import identifier columns explicitly as text if zeros matter, and check the raw output before concluding that conversion removed them.

The sample conversion, field-by-field round trip and selected rejection cases were executed in Python. Large files require a memory-conscious implementation because this small script validates all rows before writing. Column Harbor does not provide a TSV conversion service; the code here runs on your own machine.

Common questions

Can I simply change .tsv to .csv?

No. Renaming changes the filename, not the delimiter or quoting. Use a parser and writer when the receiving system actually requires comma-separated records.

Why are there still tabs in the CSV?

A tab that was part of a quoted field is data. It can remain in a CSV field because commas, not tabs, separate the output columns.

What if my TSV does not use quotes?

Use the exact rules of its exporter. This script assumes double-quoted fields with doubled quote escaping; do not apply it unchanged to a different escape convention.

Sources & method

Examples and diagrams use synthetic data. Application instructions follow the linked documentation; available menus and options can vary. Our sample checks do not establish behavior in every Excel or Google Sheets version. Read our AI-assisted editorial method.

Browse all field guides →