Defined text conversion contracts
Convert CSV to Comma-Delimited Text Without Breaking Fields
Check whether CSV already contains comma-delimited text, then normalize a known input dialect while preserving quoted commas, empty fields and multiline values.
A comma-separated CSV is already a comma-delimited text file. If another program asks for a .txt extension but accepts the same CSV syntax, copy the file with that extension without altering its contents. If your file uses semicolons, tabs or pipes between fields, parse that known input dialect and write commas as the output delimiter.
Separate the extension from the syntax
The name table.csv does not prove that commas separate its fields. Some exports use a different separator while retaining that extension. Inspect a known record or the exporting program’s settings. Also confirm whether the receiving program understands quoted fields; comma-delimited alone is not a complete format specification.
If the receiver cannot handle quotes but values contain commas or line breaks, a direct lossless conversion may be impossible under its rules. Do not delete those characters just to make an import succeed. Ask for an accepted escaping convention or choose a different supported import format.
Understand what must survive conversion
A value containing a comma stays inside a quoted field in conventional CSV. Literal quotation marks inside that field are doubled. A quoted field can also contain a line break, so counting physical lines does not necessarily count records. These rules are described in RFC 4180’s common CSV format.
This distinction explains why replacing every semicolon with a comma is unsafe. A semicolon may appear inside a quoted value and belong to the data. The parser identifies field boundaries first; the writer then chooses the necessary output quoting for the actual values.
Normalize a selected delimiter
import csv
import io
def comma_text(source, input_delimiter=","):
if not isinstance(source, str):
raise TypeError("source must be text")
if input_delimiter not in (",", ";", "\t", "|"):
raise ValueError("choose comma, semicolon, tab or pipe")
if len(source) > 1_000_000:
raise ValueError("example limit: 1,000,000 characters")
rows = list(csv.reader(io.StringIO(source, newline=""),
delimiter=input_delimiter, strict=True))
if not rows or not rows[0]:
raise ValueError("a first record is required")
width = len(rows[0])
if any(len(row) != width for row in rows):
raise ValueError("all records must have the same field count")
output = io.StringIO(newline="")
csv.writer(output, delimiter=",", lineterminator="\r\n").writerows(rows)
return output.getvalue()
The function accepts a comma, semicolon, tab or pipe as the declared input separator. It requires a first record and consistent field counts, preserves strings and emits comma-separated records ending in CRLF. It does not guess whether the first record is a header; that decision belongs to the destination contract.
For a semicolon source, call comma_text(source, ";"). For a tab source, pass "\t" as the second argument in Python code. The one-million-character limit and full in-memory table are deliberate constraints of this example, not a promise that it handles an arbitrarily large export.
Compare parsed fields instead of appearances
Suppose the semicolon-delimited input contains the fields Code, Label and Note. A record with code 00123, label North, east and note Says "ready" must preserve all three values. The comma in the label becomes a reason to quote that field in the output; it does not create a fourth field.
Include a second record with an empty final field. Its last separator remains present. Include a multiline quoted note if your actual receiver permits one. After conversion, parse the result with the output dialect and compare every field to the input table rather than relying on how a text editor wraps the lines.
Handle a file that is already comma-separated
Calling the function with a comma input delimiter can normalize optional quoting and record endings while preserving parsed field values. It is not a byte-preserving operation. If exact file bytes must remain unchanged and the recipient only requires a different name, a direct copy is the appropriate operation.
For example, an exporter may quote every field while this writer quotes only fields that need quoting. Both can represent the same table. A text comparison will show differences even when the parsed records are identical, so choose byte comparison or table comparison according to what you are verifying.
Write the selected output file
with open("source.csv", encoding="utf-8-sig", newline="") as file:
result = comma_text(file.read(), ";")
with open("comma-output.txt", "x", encoding="utf-8", newline="") as file:
file.write(result)
This example specifically assumes semicolon input. Change that argument only after identifying the actual delimiter. The new file uses UTF-8, and exclusive-create mode refuses to replace a previous output. A UTF-8 decoding failure should lead to identifying the source encoding, not silently replacing unreadable characters.
Check interpretation after syntax
A table can parse correctly while a receiving spreadsheet changes an identifier into a number or interprets a date differently. Set the destination’s column types explicitly where possible and inspect values such as 00123 and an unambiguous test date. Quoting alone does not guarantee that an importer will preserve a field’s intended type.
Keep the source and the converted file separately, record the chosen input delimiter and test two distinct records in the destination. The conversion is complete when both the file structure and the receiving interpretation match the intended table, not merely when the extension has changed.


