Home / Blog / Other text utilities

Defined text conversion contracts

Convert a Tab-Delimited TXT File to CSV With Python

Convert a defined tab-delimited text table to CSV with Python, checking headers, field counts and quoting while preserving Unicode and leading-zero identifiers.

Convert a Tab-Delimited TXT File to CSV With Python

To convert a TXT file to CSV with Python, first identify the structure inside the text file. The example below handles a tab-delimited table with a header row. It parses fields before writing comma-separated output, so a comma inside a name remains part of that value instead of becoming an extra column.

Define what TXT means for this conversion

A .txt extension does not identify a table format. The file might contain tabs, fixed-width fields, paragraphs, a log or data already separated by commas. This function expects tabs between fields, double quotes around fields when needed and the same number of fields in each record. It is not a general parser for every text document.

Inspect the source with whitespace visible or use a small known export. Do not replace every space with a comma: a name such as North Office contains a meaningful space. If your source is fixed-width, its column positions require a different parser before the CSV-writing step.

Use a parser and an explicit table contract

import csv
import io

def tsv_to_csv(source):
    if not isinstance(source, str):
        raise TypeError("source must be text")
    if len(source) > 1_000_000:
        raise ValueError("example limit: 1,000,000 characters")
    rows = list(csv.reader(io.StringIO(source, newline=""),
                           delimiter="\t", strict=True))
    if not rows or not rows[0]:
        raise ValueError("a header row is required")
    width = len(rows[0])
    if any(not name.strip() for name in rows[0]):
        raise ValueError("header names must not be blank")
    if len(set(rows[0])) != width:
        raise ValueError("header names must be unique")
    for number, row in enumerate(rows[1:], 2):
        if len(row) != width:
            raise ValueError(f"record {number}: expected {width} fields")
    output = io.StringIO(newline="")
    csv.writer(output, lineterminator="\r\n").writerows(rows)
    return output.getvalue()

The function returns CSV text and does not open or overwrite files. It requires a nonempty header with unique, nonblank names, rejects inconsistent record widths and applies a one-million-character limit suitable for this small example. Header uniqueness is exact and case-sensitive; it does not silently rename columns.

The Python CSV documentation describes reader and writer dialect parameters and the newline handling used here. The standard reader returns field strings, allowing the example to keep identifiers such as 00123 without converting them into numbers.

Read and write explicitly encoded files

from pathlib import Path

source = Path("contacts.txt").read_text(encoding="utf-8-sig")
result = tsv_to_csv(source)
with open("contacts-converted.csv", "x", encoding="utf-8", newline="") as file:
    file.write(result)

Keep both snippets in the same script, with the function defined first. The input encoding accepts UTF-8 with or without its optional initial byte-order mark. The output is UTF-8 without that marker. The exclusive-create mode refuses to replace an existing output file, so choose a fresh destination name for a repeat run.

This read_text call normalizes file line endings before conversion. If preserving embedded CRLF characters inside quoted fields is a requirement, read with open using newline="" instead. The in-memory conversion function itself preserves embedded field newlines. State which behavior the receiving system needs rather than treating byte identity as a consequence of CSV conversion.

Check values that expose common mistakes

Use a table with the headers ID, Name and Note. Set the first identifier to 00123, the name to René and the note to North, east. In the source, tabs separate the three fields. In the output, the note must be quoted because its comma is part of one field. The accent and the leading zeros should remain unchanged.

Add a second record with an empty Note. Its final field remains empty rather than disappearing. Add a literal quotation mark in another note and confirm that the CSV writer encodes it with doubled quotation marks. Compare the parsed values, since optional quoting can differ between valid CSV writers.

Handle invalid input before creating output

A record with too few or too many fields raises a ValueError that identifies the logical record number. A blank physical line is also a record with no fields and fails this strict table contract. An unterminated quoted field produces a csv.Error. These are signals to inspect the source, not reasons to drop a row silently.

The strict reader is not a complete validation of every possible business rule. The function does not check whether a phone number is usable, whether an amount is valid or whether a required identifier is empty. Add those checks to the parsed rows when the destination specification requires them.

Adapt the example deliberately

The function collects the complete table in memory and limits its character count. For larger files, use a streaming reader and a separate temporary destination, then publish the result only after validation succeeds. A direct streaming write to the final destination can leave a partial CSV when a later record fails.

Finally, load a small converted sample in the receiving program using explicit column types. A valid CSV file can still be misinterpreted by automatic date or number detection. File conversion preserves textual field values; the destination’s import settings determine how those values are subsequently used.