Plain Text Line Boundaries: Mixed line endings, Blank lines, Explicit counting rule

Split Python Plain Text into Lines Without Inventing a Trailing Record

Compare Python splitlines with split on a newline, preserve meaningful blank lines, and test empty input before counting plain-text records.

For ordinary line-oriented text, Python’s str.splitlines() avoids the extra empty element that split('\n') produces after a final newline. It also recognises more line boundaries than a single line-feed character. Choose it because those rules match your input contract, not as an automatic fix for every import format.

Suppose an application receives a pasted list of inventory names. A final Enter should terminate the last line, while an intentionally empty line between two names should remain visible for validation. These are different cases. Removing every empty string conceals the second case rather than explaining it.

Compare the actual strings before changing the parser

Run the following self-contained example with Python 3. It uses only in-memory text and assertions; it does not read or rewrite a production file. The strings deliberately include carriage returns, line feeds, an interior blank line and a Unicode line separator.

cases = [
    ('empty', '', []),
    ('terminated', 'alpha\n', ['alpha']),
    ('blank middle', 'alpha\n\nbeta\n', ['alpha', '', 'beta']),
    ('mixed endings', 'alpha\r\nbeta\rgamma\n', ['alpha', 'beta', 'gamma']),
    ('separator only', '\n', ['']),
    ('unicode boundary', 'alpha\u2028beta', ['alpha', 'beta']),
]

for label, text, expected in cases:
    actual = text.splitlines()
    assert actual == expected, (label, actual)
    print(label, repr(actual))

assert 'alpha\n'.split('\n') == ['alpha', '']
assert ''.split('\n') == ['']

text = 'alpha\r\n\r\nbeta\r'
parts = text.splitlines(keepends=True)
assert parts == ['alpha\r\n', '\r\n', 'beta\r']
assert ''.join(parts) == text
print('kept boundaries', repr(parts))

The assertions specify the intended results, not merely that the code finishes. Empty input yields no lines with splitlines. A string containing only one line break yields one empty line. A final terminator does not append an additional line, but an empty line between terminators is preserved.

Using repr makes retained control characters visible when printing the final list. Without it, a diagnostic display can hide the difference between a carriage return, a line feed and their paired sequence.

Define whether you are counting lines or valid records

len(text.splitlines()) counts the resulting line strings under these rules. It does not count valid inventory records. For example, the blank-middle fixture returns three lines but only two nonempty strings. Whether the blank line is an error, a separator or an ignored entry is an application decision.

Do not immediately append if line.strip() to a comprehension unless your specification permits discarding whitespace-only entries. That filter changes the data selection. Likewise, calling strip on each returned string changes leading and trailing spaces that may be meaningful in a field.

A practical validation process first keeps the split results, then checks each line against the documented record rules, reporting the input line number without exposing confidential contents. Only accepted records should reach a downstream system. A clean split proves neither a valid identifier nor permission to import it.

Preserve terminators only when your task needs them

The keepends=True option retains recognised boundaries on the returned strings. In the example, joining those parts reconstructs the original in-memory text exactly. This is useful when investigating a pasted payload without silently normalising its line endings.

It is not proof of byte-for-byte preservation of an original file. Text decoding and a file reader’s newline handling may already have transformed the bytes before the string reached splitlines. If original bytes matter, preserve the source separately and define encoding and newline handling before comparing results. This example intentionally makes no file-integrity claim.

Know when splitlines is too broad

The official Python splitlines reference documents boundaries including carriage return, carriage-return/line-feed, vertical tab, form feed and Unicode separators. If a protocol permits only a particular delimiter, that broader behaviour might be wrong: inspect and enforce the protocol instead of accepting every recognised boundary.

Structured records need their own parser. A quoted CSV field may legally contain an embedded newline, so splitting the text first can break one record into several pieces. See preserving multiline CSV fields with Python’s CSV parser for that separate problem. General JSON documents also should not be converted into line-based records merely because parsing failed.

FAQ and a small acceptance checklist

Does splitlines remove all empty lines? No: interior empty lines are retained. Does keepends validate line endings? No: it preserves recognised boundaries, rather than enforcing one permitted delimiter.

Before deploying a line-based importer, test empty input, a final terminator, two consecutive terminators, whitespace-only lines and each supported boundary. Decide explicitly whether unexpected separators are rejected and whether blank lines count as errors. Keep those decisions in tests so a later cleanup does not silently change record counts.

Educational use only. Test carefully before production. The site owner is not responsible for misuse, damage, data loss or production issues.

Leave a Reply

Your email address will not be published. Required fields are marked *