The str.splitlines(keepends=False) method splits a string into a list of lines, breaking at universal line boundary boundaries including Unix \n, Windows \r\n, classic Mac \r, and standard Unicode line breaks.
The Trailing Newline Invariant: Unlike str.split('\n'), which creates a spurious empty string element when a text block ends with a terminal newline, splitlines() recognizes the final newline as a line terminator rather than a delimiter, returning clean line collections without trailing artifacts.
The method signature defined in the Python runtime is:
str.splitlines(keepends=False)
| Parameter | Type | Default | Description |
|---|---|---|---|
keepends |
bool |
False |
Optional. When set to True, line break characters (such as \n or \r\n) are retained at the end of each resulting string element in the list. |
list of strings. For an empty input string "", splitlines() returns an empty list [], regardless of whether keepends is True or False.
Python's str.splitlines() conforms to Unicode standard line breaking (UAX #14), recognizing both standard ASCII control breaks and advanced Unicode paragraph breaks:
| Escape Sequence | Description | Operating System / Platform Standard |
|---|---|---|
\n |
Line Feed (LF, ASCII 10) | Linux, macOS, Unix, modern web protocols |
\r\n |
Carriage Return + Line Feed (CRLF, ASCII 13 + 10) | Microsoft Windows, DOS, HTTP/MIME protocol headers |
\r |
Carriage Return (CR, ASCII 13) | Classic Mac OS (pre-OS X), serial telemetry streams |
\v / \x0b |
Line Tabulation / Vertical Tab (VT, ASCII 11) | Legacy line printers, typesetting |
\f / \x0c |
Form Feed (FF, ASCII 12) | Page break controls in ASCII text documents |
\x1c, \x1d, \x1e |
File, Group, Record Separators (ASCII 28–30) | Mainframe data streams, EDI exchange |
\x85 |
Next Line (NEL, EBCDIC) | IBM mainframe systems |
\u2028 |
Line Separator | Unicode line break standard |
\u2029 |
Paragraph Separator | Unicode paragraph break standard |
When encountering Windows \r\n, splitlines() treats the pair as a single atomic delimiter rather than two separate line breaks.
Many developers mistakenly use text.split('\n') under the assumption that it behaves identically to splitlines(). This creates two severe production bugs:
Standard text files typically end with a trailing newline. Notice the discrepancy:
file_data = "Header\nRecord 1\nRecord 2\n"
# 1. Using split('\n') produces a spurious trailing empty string
naive_split = file_data.split("\n")
print("split('\\n'): ", naive_split) # ['Header', 'Record 1', 'Record 2', '']
# 2. Using splitlines() recognizes trailing newline as line terminator
robust_split = file_data.splitlines()
print("splitlines(): ", robust_split) # ['Header', 'Record 1', 'Record 2']
Expected Output:
split('\n'): ['Header', 'Record 1', 'Record 2', '']
splitlines(): ['Header', 'Record 1', 'Record 2']
When reading Windows-generated files (CRLF), split('\n') splits only on \n, leaving stray \r characters attached to the end of every parsed line:
windows_data = "Row1\r\nRow2\r\nRow3"
# split('\n') leaves unwanted \r in strings
bad_lines = windows_data.split("\n")
print("split('\\n') with CRLF: ", bad_lines) # ['Row1\r', 'Row2\r', 'Row3']
# splitlines() consumes the full \r\n cleanly
good_lines = windows_data.splitlines()
print("splitlines() with CRLF: ", good_lines) # ['Row1', 'Row2', 'Row3']
Expected Output:
split('\n') with CRLF: ['Row1\r', 'Row2\r', 'Row3']
splitlines() with CRLF: ['Row1', 'Row2', 'Row3']
| Input String | str.splitlines() |
str.split('\n') |
|---|---|---|
"" (Empty string) |
[] (Zero lines) |
[""] (One empty line) |
"alpha\n" (Trailing newline) |
['alpha'] (1 element) |
['alpha', ''] (2 elements) |
"a\r\nb" (Windows CRLF) |
['a', 'b'] (Stripped) |
['a\r', 'b'] (Leaves carriage return) |
"a\rb" (Classic Mac CR) |
['a', 'b'] (Recognized) |
['a\rb'] (Ignored, single string) |
Setting keepends=True retains each line's exact native line terminator. This is indispensable when performing exact byte-level text reconstructions, computing file hash digests per line, or executing diff algorithms:
mixed_stream = "First line\nSecond line\r\nThird line"
lines_with_breaks = mixed_stream.splitlines(keepends=True)
print("With keepends=True:", lines_with_breaks)
# Reconstructing original text perfectly:
reconstructed = "".join(lines_with_breaks)
print("Reconstruction matches:", reconstructed == mixed_stream)
Expected Output:
With keepends=True: ['First line\n', 'Second line\r\n', 'Third line']
Reconstruction matches: True
When handling file uploads or multiline textarea submissions from varying operating systems, convert mixed line breaks to clean POSIX \n lines:
def normalize_line_endings(content: str) -> str:
# splitlines() consumes \r, \n, and \r\n uniformly
lines = content.splitlines()
return "\n".join(lines) + ("\n" if content.endswith(("\n", "\r")) else "")
raw_payload = "Name: Alice\r\nRole: Admin\rDepartment: Engineering\nStatus: Active"
posix_text = normalize_line_endings(raw_payload)
print("Normalized output:\n", repr(posix_text))
Expected Output:
Normalized output:
'Name: Alice\nRole: Admin\nDepartment: Engineering\nStatus: Active'
Splitting code snippets into numbered, syntax-aligned lines:
def format_code_block(code: str, start_num: int = 1) -> str:
lines = code.splitlines()
max_digits = len(str(start_num + len(lines)))
formatted = []
for idx, line in enumerate(lines, start=start_num):
formatted.append(f"{idx:>{max_digits}} | {line}")
return "\n".join(formatted)
snippet = """def add(a, b):
# Sum two values
return a + b"""
print(format_code_block(snippet))
Expected Output:
1 | def add(a, b):
2 | # Sum two values
3 | return a + b
Grouping multiline log records where indented lines belong to the previous header:
def parse_grouped_logs(log_text: str) -> list[dict[str, str]]:
entries = []
current_entry = None
for line in log_text.splitlines():
if not line.strip():
continue
if line.startswith((" ", "\t")):
# Indented continuation line (stack trace)
if current_entry:
current_entry["details"].append(line.strip())
else:
if current_entry:
entries.append(current_entry)
current_entry = {"header": line, "details": []}
if current_entry:
entries.append(current_entry)
return entries
raw_log = """2026-10-13 12:00:01 ERROR Connection failed
at socket.connect() line 45
at client.init() line 12
2026-10-13 12:00:02 INFO System healthy"""
logs = parse_grouped_logs(raw_log)
print("Parsed log entries:", len(logs))
print("First entry stack trace lines:", len(logs[0]["details"]))
Expected Output:
Parsed log entries: 2
First entry stack trace lines: 2
Unlike str.split() with sep=None (which collapses adjacent spaces), splitlines() does not discard empty lines. Each consecutive newline is treated as an empty line:
data = "Line1\n\n\nLine2"
lines = data.splitlines()
print(lines) # ['Line1', '', '', 'Line2']
If your application requires ignoring blank lines, filter with a list comprehension: [line for line in data.splitlines() if line.strip()].
Below is a production-grade utility class, LineProcessor, demonstrating cross-platform newline normalization, keepends preservation, and comprehensive test assertions:
class LineProcessor:
@staticmethod
def get_clean_lines(text: str) -> list[str]:
"""Splits text and strips surrounding whitespace per line."""
return [line.strip() for line in text.splitlines() if line.strip()]
@staticmethod
def reconstruct_lines(lines_with_breaks: list[str]) -> str:
"""Reassembles lines preserved via keepends=True."""
return "".join(lines_with_breaks)
@staticmethod
def count_paragraphs(text: str) -> int:
"""Counts non-empty lines separated by universal line breaks."""
return len([ln for ln in text.splitlines() if ln.strip()])
# ==========================================
# SELF-CHECKING UNIT ASSERTIONS
# ==========================================
processor = LineProcessor()
# 1. Empty string yields empty list
assert "".splitlines() == []
assert "".splitlines(keepends=True) == []
# 2. Trailing newline behavior (no spurious empty item)
assert "Hello\nWorld\n".splitlines() == ["Hello", "World"]
assert "Hello\nWorld\n".split("\n") == ["Hello", "World", ""]
# 3. Universal newlines: CRLF, CR, LF
mixed = "Line1\nLine2\r\nLine3\rLine4"
assert mixed.splitlines() == ["Line1", "Line2", "Line3", "Line4"]
# 4. keepends=True round-trip integrity
lines_kept = mixed.splitlines(keepends=True)
assert lines_kept == ["Line1\n", "Line2\r\n", "Line3\r", "Line4"]
assert processor.reconstruct_lines(lines_kept) == mixed
# 5. Unicode line separator (\u2028) support
unicode_lines = "Paragraph1\u2028Paragraph2"
assert unicode_lines.splitlines() == ["Paragraph1", "Paragraph2"]
# 6. Clean line filtering
doc = " Alpha \n\n Beta \n\n\n Gamma \n"
assert processor.get_clean_lines(doc) == ["Alpha", "Beta", "Gamma"]
assert processor.count_paragraphs(doc) == 3
print("Clean lines extracted: ", processor.get_clean_lines(doc))
print("Paragraphs counted: ", processor.count_paragraphs(doc))
print("All str.splitlines() unit assertions passed successfully!")
Open in Google Colab View on GitHub
Run all universal newline splitting examples interactively, test keepends round-trip reassembly, inspect Windows CRLF vs Unix LF behavior, and parse multiline stack traces in Google Colab.
Author & Instructor at plus2net
I write and maintain practical tutorials on Python, PHP, SQL, JavaScript, HTML, jQuery, and web development at plus2net. The tutorials focus on clear explanations, working examples, and code that readers can test and adapt while learning.