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 sample | Meaning | Conversion requirement |
|---|---|---|
| Tab outside quotes | Field separator | Becomes a comma separator |
| Tab inside quotes | Part of the note | Remain inside that field value |
| Doubled quote inside a quoted field | One quote character | Preserve through CSV quoting |
| Newline inside quotes | Part of the note | Remain inside one logical record |
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.
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.
id,note,status
00123,"Blue, ""large"" mug",ready
00456,left right,
00789,"line one
line two",holdRelated 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.
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. The code here runs on your own machine. Column Harbor also offers a local browser converter for quoted, tab-delimited UTF-8 input; choose its input and output delimiters explicitly. Neither method establishes compatibility with every TSV dialect.
Try the quoted TSV example in your browser
Open the Workspace format converter linked above for a browser-only path. Use this guide's quoted UTF-8 example, not a file whose escape rules are unknown. The browser tool reports malformed quotes and inconsistent field counts instead of padding or discarding records.
Delimited downloads have an optional spreadsheet-protection setting that prefixes formula-like values with an apostrophe. That setting changes those values, including negative numbers, and is not a security guarantee. The sample here has no formula-like prefixes. For other files, review that export choice and still import identifier columns as text in the receiving spreadsheet.
- Download this page's TSV sample or copy its worked example, then open the browser format converter.
- Load or paste the sample, choose Tab (Excel paste) as Input delimiter and select Read table. Expect three data records and three columns, not one record for each physical line.
- Choose Comma as Output format and select Convert table. Inspect the preview before downloading.
- Check all three IDs, the quote in the first note, the literal tab in the second note, its empty status, and the newline in the third note. These field values should match the Expected CSV field content above. Download the CSV and use an explicit text import if opening it in a spreadsheet.
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.