What ends a line¶
Level: 201 · for Python programmers
One line: str.splitlines() splits on ten different characters, the file it came from splits on three, and re in MULTILINE mode splits on one — so one program can get three line counts out of the same bytes, and three of Python's ten are boundaries no other language recognizes.
Before a language can hand you a line it has to answer one question: which characters end one. Everybody agrees about \n, most agree about \r\n, and past that the agreement stops — not between languages, and not inside Python either. These two lines look like the same intent:
On ASCII text with Unix endings they return the same list, which is why the difference survives every test anyone writes. On a report with a form feed between pages, or a legacy extract with 0x1e between records, or a Windows file whose second field happens to hold a 0x85, they do not — and nothing on screen says so, because every character involved is invisible.
Verified output of what_ends_a_line_py.py — regenerated by tools/run_examples.py, never hand-typed.
1. EVERY CODE POINT, AND THE TEN THAT SPLIT
Scanned 1,112,064 code points. 10 of them end a line:
code point bidi isspace name
------------------------------------------------------------------
LF U+000A B True LINE FEED
VT U+000B S True LINE TABULATION
FF U+000C WS True FORM FEED
CR U+000D B True CARRIAGE RETURN
FS U+001C B True INFORMATION SEPARATOR FOUR (file)
GS U+001D B True INFORMATION SEPARATOR THREE (group)
RS U+001E B True INFORMATION SEPARATOR TWO (record)
NEL U+0085 B True NEXT LINE
LS U+2028 WS True LINE SEPARATOR
PS U+2029 B True PARAGRAPH SEPARATOR
Plus one two-character sequence: CR LF counts once, not twice.
And one near miss -- U+001F, unit separator, is whitespace to
isspace() and a separator to split(), and is not a line boundary.
2. THE SAME TEXT, THREE ANSWERS
text = 'a\nb\x0bc\rd\x1ce\x85f\u2028g'
Seven pieces, separated by six different candidates: LF VT CR FS NEL LS.
text.splitlines() 7 lines ['a', 'b', 'c', 'd', 'e', 'f', 'g']
iterating those bytes 3 lines ['a\n', 'b\x0bc\n', 'd\x1ce\x85f\u2028g']
re.findall('^.*', re.M) 2 lines ['a', 'b\x0bc\rd\x1ce\x85f\u2028g']
Nothing about the text changed. str.splitlines() knows ten
boundaries, universal newlines knows three (LF, CR, CRLF), and
re knows exactly one -- \n -- even in MULTILINE mode.
3. WHERE THE TEN COME FROM
Bidi_Class B ('paragraph separator') holds 7 code points:
U+000A U+000D U+001C U+001D U+001E U+0085 U+2029
Of those, 0 are missing from splitlines().
splitlines() adds 3: U+000B LINE TABULATION, U+000C FORM FEED, U+2028 LINE SEPARATOR
So the rule is: every character Unicode's bidirectional algorithm
calls a paragraph separator, plus vertical tab, form feed and
LINE SEPARATOR. FS, GS and RS are in the set because Unicode
puts them there -- they were ASCII's file, group and record
separators, and Unicode classifies all three as paragraph ends.
4. bytes.splitlines() KNOWS TWO
Over all 256 byte values, bytes.splitlines() splits on: 0x0A, 0x0D
b'a\r\nb'.splitlines() = [b'a', b'b'] (CRLF, still one)
as str as utf-8 bytes
----------------------------------------
VT 2 1
RS 2 1
NEL 2 1
A bytes object does not know which table produced it, so it
answers for ASCII only. Decode first and the answer changes.
5. THE CODEC DECIDES WHETHER THERE IS A LINE THERE
codec decodes to lines
--------------------------------------------
latin-1 'amount\x85date' 2
cp1252 'amount…date' 1
cp437 'amountàdate' 1
cp037 'amount\x85date' 2
The first three read the same bytes: b'amount\x85date'
The fourth is EBCDIC: b'\x81\x94\x96\xa4\x95\xa3\x15\x84\x81\xa3\x85'
Byte 0x85 is NEXT LINE in latin-1, a horizontal ellipsis in
cp1252 and an a-grave in cp437. Same byte, same file, and only
the codec you passed to .decode() decides whether your data has
two lines or one. The last row is why NEL is in the set at all:
byte 0x15 is the mainframe's newline, and cp037 maps it to U+0085.
6. splitlines() IS NOT split('\n')
input splitlines() split('\n')
--------------------------------------------------------------
'' [] ['']
'a' ['a'] ['a']
'a\n' ['a'] ['a', '']
'a\n\n' ['a', ''] ['a', '', '']
'a\r\nb' ['a', 'b'] ['a\r', 'b']
'a\x0bb' ['a', 'b'] ['a\x0bb']
keepends=True on 'a\r\nb': ['a\r\n', 'b']
splitlines() treats a trailing terminator as ending the last
line rather than starting an empty one, and returns [] for the
empty string where split() returns ['']. That difference is why
a file's last line does not arrive twice -- and why reaching for
split('\n') to dodge the FS problem adds a phantom empty line.
str.splitlines() recognizes ten characters, and the program checks every one of the 1,112,064 code points to prove there is no eleventh. Four of them are the ones you would guess — \n, \r, \v, \f — plus the \r\n pair, counted once. Three more are Unicode's own separators: NEL (U+0085), LINE SEPARATOR (U+2028) and PARAGRAPH SEPARATOR (U+2029). And three are the ASCII information separators: file (0x1c), group (0x1d) and record (0x1e), which is where Python parts company with every other language on this page.
The rule behind the ten is not arbitrary, and it is worth knowing because it predicts the surprising members. Take every code point Unicode's bidirectional algorithm classifies as a paragraph separator — Bidi_Class B, which is exactly seven characters — and add vertical tab, form feed and LINE SEPARATOR. That is the set, checked in section 3 against the whole code space with nothing left over in either direction. FS, GS and RS are in splitlines() because Unicode itself calls them paragraph ends; Python is following the table rather than inventing a rule. Their sibling US (unit separator, 0x1f) is Bidi_Class S, so it is not a line boundary — even though isspace() is True for it and str.split() with no argument does split there. One family, four characters, and the fourth behaves differently from the other three.
Universal newlines is a smaller set, and it is the one your file actually uses. Text mode translates \r\n and \r into \n on the way in (the glossary entry ↗ is the definition), so iterating a file gives you three boundaries, not ten. The gap between the two is the practical bug: read().splitlines() and list(open(...)) are not interchangeable, and the seven characters where they differ are precisely the ones you cannot see. If you want the file's own answer, iterate the file. If you want splitlines()'s answer, say so on purpose — and know that you have just opted into splitting on three control characters that a legacy format may be using for something else entirely.
re has the narrowest answer of the three: \n and nothing else. In MULTILINE mode ^ matches at the start and after a \n; $ matches before a \n; . refuses \n and accepts every other candidate on the list. So a regex-based line loop over the same text sees two lines where splitlines() sees seven. Python's re has no \R — the "any line break" escape that Perl, Ruby, Java and PCRE2 all provide, and which Rust's regex crate also refuses ("unrecognized escape sequence") — so there is no way to ask re for a wider answer without spelling it out yourself. splitlines()'s own set, written as a pattern, is r"\r\n|[\n\v\f\r\x1c\x1d\x1e\x85\u2028\u2029]" — checked against splitlines() over all 1,112,064 code points while writing this page, with no disagreement in either direction.
bytes.splitlines() knows two, and that is the type boundary again rather than an inconsistency. Over all 256 byte values it splits on 0x0a and 0x0d only (plus the pair). A bytes object does not know which encoding produced it, so it cannot know whether 0x85 is a NEL or the second byte of something else, and it declines to guess — the same reasoning that makes the eight bytes predicates ASCII-only.
Which means the codec decides whether there is a line there at all. Section 5 is one byte, 0x85, read four ways: NEXT LINE in latin-1 (two lines), a horizontal ellipsis in cp1252 (one line), à in cp437 (one line), and — going the other way — EBCDIC's newline 0x15, which cp037 maps to U+0085, giving two lines from bytes that contain no 0x85 at all. Decoding is not a formality you do before the real work; on this question it is the real work. That is also the answer to "why is NEL in the set": it exists because IBM mainframes ended lines with it, and Python honours it so that a decoded EBCDIC extract splits correctly.
And splitlines() is not split("\n"). The empty string gives [] rather than [""], and a trailing terminator ends the last line rather than starting an empty one — which is why a file's final line does not arrive twice. The temptation, on meeting the FS surprise, is to swap in split("\n") to get a narrower rule; that also swaps in a phantom empty element at the end of every file that ends properly. splitlines(keepends=True) is the version that keeps the terminator, and it keeps \r\n as one piece.
How the other languages answer¶
Every language draws its line somewhere on the same ladder, and the rungs are worth knowing because the answer is rarely documented where you are standing. Measured here rather than remembered:
n boundaries who stops there
-- -------------------------------------- ---------------------------------------------
1 LF Python re (MULTILINE), .NET Regex (Multiline),
C fgets, awk RS, Ruby String#lines, Perl split
1 LF, and a trailing CR trimmed off Rust str::lines, Go bufio.ScanLines
3 LF CR CRLF Python text mode, Python bytes.splitlines,
.NET StringReader.ReadLine, Java String.lines
4 LF CR LS PS JavaScript — the language's own line terminators
6 LF CR CRLF NEL LS PS Java Pattern in MULTILINE mode
7 LF CR CRLF NEL LS PS FF the Unicode readline recommendation (5.8 R4);
.NET 6+ ReplaceLineEndings (docs)
8 ... + VT \R in Perl, Ruby, PCRE2 and Java;
Swift Character.isNewline
10 ... + FS GS RS Python str.splitlines()
run with: Python 3.14.7, rustc 1.98.0, go1.25.5, node v20.20.2, ruby 2.6.10,
perl 5.42.0, Swift 6.3.3, OpenJDK 25.0.4.1, .NET 5.0.5,
clang 21.0.0, awk 20200816, pcre2grep 10.48
Nobody else splits on FS, GS and RS. Python is alone on the top rung, and the gap is not an oversight by the others — the Unicode standard's own newline guidelines ↗ (§5.8, Recommendation R4) say a readline function should stop at LF, CR, CRLF, NEL, LS, FF and PS, and that list is seven characters with no information separators in it. The \R escape adds VT for eight. Python adds three more on top of that, from the bidi table rather than from the newline recommendation.
Rust and Go took the opposite decision and are the strictest here. Both split on \n and then trim one trailing \r, so \r\n works and a lone \r is ordinary data — a Mac-Classic-era file is one long line to both of them. Rust reached that behaviour deliberately and it is worth reading how: pre-1.4 lines() removed all of a one-byte terminator and half of a two-byte one, which is the shape that produces a bug rather than merely permitting one (RFC 1212 ↗ is the whole argument). Go's bufio.ScanLines lands in the same place; its ReadString('\n') keeps the terminator instead of dropping it, which is the other coherent design. And Rust is the one language here that considered Python's answer explicitly and turned it down: the RFC thread spent longer on "should lines() know about \v, \f, NEL, LS and PS?" than on the change it was called for, and settled on no — a protocol may define \n as its separator and allow arbitrary Unicode inside a line, and a Unicode-aware lines() would corrupt exactly that, rarely enough to reach production. The same argument applies to splitlines(); Python simply made the other call, and made it the default.
JavaScript is the odd one out, because its four terminators are a property of the language, not of a library call. LF, CR, LS and PS are what a . in a regex refuses to match and what ends a // comment — and because LS and PS were legal inside a JSON string but illegal inside a JavaScript string literal, JSON was not a subset of JavaScript until ES2019 ↗ fixed it. There is no lines() in the standard library at all; everyone writes s.split(/\r?\n/) and quietly picks rung one.
Ruby and Perl put the answer in a global variable. The input record separator — $/ in both — defaults to "\n", so String#lines and split split there; assign it and every line-reading operation in the program changes behaviour. That is the most configurable design here and the least local: the answer to "what ends a line" is not in the call you are reading.
Swift is the only one where a line ending is a single character. Character.isNewline covers the Unicode eight, and because a Swift Character is a grapheme cluster, "\r\n" is one Character — "a\r\nb".count is 3. Everywhere else on this page a CRLF is two units that a library has to special-case; in Swift the text model already merged them.
Java and .NET both disagree with themselves, the same way Python does. Java's String.lines() recognizes three, Pattern in MULTILINE mode recognizes six, and \R recognizes eight — three answers in one standard library, from a language usually accused of being over-specified, and all three measured here rather than read off the spec. Which of the three you ask matters as much as what decoded the file, and section 5's byte shows both: decoded as latin-1, 0x85 is a NEL that Pattern and \R split on and lines() does not; decoded as windows-1252 it is an ellipsis that none of them split on. A String is UTF-16 by the time any of them sees it, so the decoding half was settled before they ran, and since JEP 400 ↗ the default decoder is UTF-8 rather than the platform locale's: file.encoding stays UTF-8 on this machine even under LC_ALL=C, where native.encoding, the locale's own answer, says US-ASCII. Before Java 18 the default was the locale's, which -Dfile.encoding=COMPAT still restores, so the same bytes could become a line boundary to Pattern on one computer and an ellipsis on another. .NET's StringReader.ReadLine recognizes three while .NET 6's ReplaceLineEndings recognizes seven and says in its documentation ↗ exactly which Unicode recommendation it is implementing, which is the good practice the rest of this table is missing.
So Python is not unusual for having several answers. It is unusual for its widest one being the widest anywhere, and for that width being the default on the friendliest-looking method of the three.
If you are coming from ABAP¶
ABAP never guesses, and that is the whole difference. SPLIT text AT cl_abap_char_utilities=>newline INTO TABLE lt_lines names the separator explicitly, so there is no ladder to be on and no surprise from a 0x1e in the data — but there is also no fallback, so a CRLF file split at newline leaves a trailing CR welded to every field, which compares unequal to everything and prints as nothing. The constant for the other case is cl_abap_char_utilities=>cr_lf, and choosing between them means knowing what produced the file. Coming the other way, the Python habit worth unlearning is trusting splitlines() on data that came off a mainframe: an EBCDIC extract decoded with cp037 splits on NEL correctly, and the same bytes read through a text-mode file object do not split there at all. (Not machine-checked — CI cannot run ABAP.)
Try it¶
- Take a file you already have and print
len(open(p).read().splitlines())next tolen(list(open(p))). On most files the two agree — then append a form feed to one line and watch them stop agreeing. Which of the two is the answer you wanted? splitlines()splits onFS,GSandRSbut notUS. Find out whatstr.split()with no argument does with all four, and then work out which of the two methods you have been reaching for when you write.split()on a field of unknown provenance.- Write the regex that gives
rethe Unicode answer — the eight-character\Rset — and check it againstsplitlines()over a string containing all ten candidates. Which three boundaries does your regex miss, and would you actually want them?
See also¶
- Encode and decode — the step that decides whether byte
0x85is a line break or an ellipsis stris notbytes— whybytes.splitlines()can only answer for ASCII- Is it a letter? — the same shape one method along: a friendly-looking predicate implementing a Unicode table rather than the rule you meant
- Counting characters — the other question with four defensible answers
- The crosswalk — which idea lives in which library
- CRLF vs LF ↗ — the repair side:
dos2unix,tr -d '\r',newline='', and git'sautocrlf - "Supports Unicode" is a level, not a yes ↗ — why
restops at one boundary andsplitlines()does not: UTS #18 calls it RL1.6, and it is one of five Level 1 requirements Python'sremisses - Control characters ↗ — what
FS,GS,RSandUSwere designed to do before anything split lines on them - RFC 1212 — how Rust's
lines()learned about\r\n↗ — the same decision, argued in public, in a language that took the narrow rung str.splitlines()in the Python docs ↗ — the table this page is unpacking, and the source of the phrase "universal newlines"