Read Text Files with Explicit Encoding and Line Rules
Decode UTF-8 bytes and split lines consistently across LF, CRLF, and CR endings.
Example check: Worked and guided code samples are checked against CPython 3.13. Practice uses Pyodide 0.29.5 (Python 3.13) in your browser. How we check lessons
Video lesson: Read Text Files with Explicit Encoding and Line Rules
The preview is stored on this site. YouTube loads only when you press Play. Watch on YouTube
Jump to a chapter
Read the video transcript
Select a timestamp to watch that moment on YouTube.
- 00:00
A text file is stored as bytes. Reading it requires an encoding, and line endings can differ across systems: LF, CRLF, and older CR. A program that splits only on '\n' may leave a trailing carriage return in each Windows-style line. The first example prints the result of splitting a CRLF string on LF alone. The remaining carriage-return characters appear as escaped backslash-r sequences. That is a common source of failed string comparisons when data crosses operating systems or is read in binary mode. A visually ordinary text file may hide a carriage return before every line feed.
- 00:41
We will make those characters visible, then choose an explicit decoding rule for bytes that are supposed to be UTF-8. Before the run, predict the elements of b'A\r\nB\n'.decode('utf-8').split('\n'). The first element retains a carriage return, and the final separator creates an empty element. Keep that prediction beside the output so invisible characters do not disappear in ordinary printing. Python's text I/O normally translates line endings when newline=None. Explicitly passing encoding='utf-8' makes the byte-to-text rule stable across machines.
- 01:23
In a browser exercise with no physical file, bytes.decode('utf-8') and str.splitlines() demonstrate the same two decisions separately. splitlines() recognizes LF, CRLF, and CR boundaries and returns logical lines without their separators. The browser task works on bytes to make the decoding step explicit. For a physical text file, open with a chosen encoding and newline behavior so the contract is visible in the call. The starter splits on LF only and leaves a carriage return attached to A. That extra character can break equality checks or produce odd output after data is moved between systems.
- 02:05
The failed result is a list with a hidden carriage return and an unwanted final empty string. Print repr of each element if the screen makes the characters hard to see. This is a logical-line bug, even though the program exits without an exception. Do not silently replace malformed bytes unless the application has an explicit data-recovery rule. Strict UTF-8 decoding raises UnicodeDecodeError and preserves the fact that the file does not match the contract. Once decoded, splitlines() recognizes common line boundaries without leaving '\r' attached to a CRLF line. The second run encodes café into UTF-8 bytes and decodes it back to characters.
- 02:50
The byte representation includes two bytes for é, so byte positions do not match character positions. Splitting raw bytes might work for ASCII line-feed bytes, but it leaves character interpretation unresolved. Use data.decode('utf-8') as an explicit first step. This lesson's format contract says input is UTF-8 and rejects other encodings; it does not guess. A real application might have a documented legacy encoding, but that should be a separate, intentional choice rather than an environment-dependent default. The diagnosis prints both the raw split and splitlines result.
- 03:30
It also shows café as bytes and as text. The two representations serve different layers and should not be conflated. The UTF-8 bytes for café show a second boundary: bytes must be decoded before text line handling. Compare that output with the raw split. Once the encoding is known, splitlines recognizes CRLF, LF, and CR without keeping their terminators. Implement read_utf8_lines(data: bytes) by decoding UTF-8 strictly and returning a list of logical lines without newline characters. Handle LF, CRLF, and CR; an invalid UTF-8 sequence should raise UnicodeDecodeError.
- 04:13
The prepared call must print ['A', 'B']. Decode with data.decode('utf-8'), then call splitlines() on the text. The final run passes ff, which is not a valid standalone UTF-8 byte. Strict decoding raises UnicodeDecodeError. Silently replacing it with a placeholder would make the line look processed even though the source did not meet the promised format. The fix is intentionally short: decode strictly, then split logical lines. For an actual file, open with encoding='utf-8' and an intentional newline policy. Do not infer the encoding from the local machine. Now read the exact b'A\r\nB\n' sample through strict UTF-8 decoding and
- 04:56
splitlines. The result should contain only A and B. This function deliberately does not trim ordinary spaces inside a line, because line endings and content are different concerns. The task returns clean lines for mixed line endings and refuses invalid UTF-8. A real file version should use open(path, 'r', encoding='utf-8', newline=None) or pathlib.Path.open with the same parameters. Binary formats should remain in binary mode rather than being forced through a text decoder. The checked function handles empty input, CRLF, LF, and CR while preserving valid
- 05:38
Unicode text. In file-based code, errors='replace' can be useful for a deliberate salvage workflow, but then the caller must know that information was lost. Here failure is the honest result for invalid bytes. splitlines recognizes LF, CRLF, and CR boundaries. An explicit encoding makes the data contract portable. The last run covers CR-only input, valid non-ASCII UTF-8, and an invalid byte. An error on malformed input is useful information; a replacement character would hide a broken contract. When debugging a real file, inspect a tiny sample in binary mode first.
- 06:23
Print repr of the bytes and the decoded text, then decide whether universal newline handling is appropriate. This avoids changing the data before you understand it. Finally, test a CR-only separator, a valid accented word, and b'\xff'. The last input must raise UnicodeDecodeError. Replacing invalid bytes would conceal a format violation, while guessing another encoding would make results depend on the environment. Check whether an empty input returns an empty list and whether spaces inside an otherwise valid line remain intact. Neither case should be mistaken for a line-ending problem.
Understand the concept
A text file is stored as bytes. Reading it requires an encoding, and line endings can differ across systems: LF, CRLF, and older CR. A program that splits only on '\n' may leave a trailing carriage return in each Windows-style line.
Python's text I/O normally translates line endings when newline=None. Explicitly passing encoding='utf-8' makes the byte-to-text rule stable across machines. In a browser exercise with no physical file, bytes.decode('utf-8') and str.splitlines() demonstrate the same two decisions separately.
Do not silently replace malformed bytes unless the application has an explicit data-recovery rule. Strict UTF-8 decoding raises UnicodeDecodeError and preserves the fact that the file does not match the contract. Once decoded, splitlines() recognizes common line boundaries without leaving '\r' attached to a CRLF line.
The task returns clean lines for mixed line endings and refuses invalid UTF-8. A real file version should use open(path, 'r', encoding='utf-8', newline=None) or pathlib.Path.open with the same parameters. Binary formats should remain in binary mode rather than being forced through a text decoder.
- Files contain bytes
- Choose an encoding explicitly
- splitlines handles common newline conventions
- Strict decoding reveals invalid input
See it step by step
Read the code, predict the output, then compare it with the result.
01. Spot a hidden CR
text='A\r\nB\r\n'
print(text.split('\n'))
print(text.splitlines())['A\r', 'B\r', '']
['A', 'B']splitlines removes different line boundary forms.
02. Round-trip UTF-8 text
name='caf\u00e9'
raw=name.encode('utf-8')
print(raw)
print(raw.decode('utf-8'))b'caf\xc3\xa9'
caféThe bytes and characters are different representations of the same text.
03. Reject malformed bytes
try:
b'\xff'.decode('utf-8')
except UnicodeDecodeError:
print('invalid utf-8')invalid utf-8Strict decoding surfaces an encoding mismatch.
A closer look
Follow the reasoning, inspect each result, then try the suggested changes in the console below.
Make a carriage return visible
The first example prints the result of splitting a CRLF string on LF alone. The remaining carriage-return characters appear as escaped backslash-r sequences. That is a common source of failed string comparisons when data crosses operating systems or is read in binary mode.
splitlines() recognizes LF, CRLF, and CR boundaries and returns logical lines without their separators. The browser task works on bytes to make the decoding step explicit. For a physical text file, open with a chosen encoding and newline behavior so the contract is visible in the call.
text='A\r\nB\r\n'
print(text.split('\n'))
print(text.splitlines())['A\r', 'B\r', '']
['A', 'B']- Compare split('\n') with splitlines().
- Notice the leftover carriage returns.
- Use logical lines for the checked result.
Decode UTF-8 before splitting text
The second run encodes café into UTF-8 bytes and decodes it back to characters. The byte representation includes two bytes for é, so byte positions do not match character positions. Splitting raw bytes might work for ASCII line-feed bytes, but it leaves character interpretation unresolved.
Use data.decode('utf-8') as an explicit first step. This lesson's format contract says input is UTF-8 and rejects other encodings; it does not guess. A real application might have a documented legacy encoding, but that should be a separate, intentional choice rather than an environment-dependent default.
name='caf\u00e9'
raw=name.encode('utf-8')
print(raw)
print(raw.decode('utf-8'))b'caf\xc3\xa9'
café- Print the encoded bytes and decoded text.
- Name the required encoding in the function contract.
- Split lines after decoding.
Surface malformed input instead of hiding it
The final run passes ff, which is not a valid standalone UTF-8 byte. Strict decoding raises UnicodeDecodeError. Silently replacing it with a placeholder would make the line look processed even though the source did not meet the promised format.
The checked function handles empty input, CRLF, LF, and CR while preserving valid Unicode text. In file-based code, errors='replace' can be useful for a deliberate salvage workflow, but then the caller must know that information was lost. Here failure is the honest result for invalid bytes.
try:
b'\xff'.decode('utf-8')
except UnicodeDecodeError:
print('invalid utf-8')invalid utf-8- Run one invalid byte case.
- Observe UnicodeDecodeError.
- Keep the task's strict decoding behavior.
Try it in Python
Edit the example and run it. Python starts in your browser the first time you click Run.
Python console
Ready to runNeed input()? Add one value per line
Your output appears here.
Need a hint?
Decode with data.decode('utf-8'), then call splitlines() on the text.
Complete the task and select Check task to verify your code.
Quick quiz
Three questions. You can change your answers and try again.
Typical mistakes
Everyone meets these errors. See what causes them and how to fix them.
Splitting only on LF
text.split('\n')text.splitlines()What happens: Windows-style lines retain '\r'.
splitlines recognizes LF, CRLF, and CR boundaries.
Omitting the encoding
open(path).read()open(path, encoding='utf-8', newline=None)What happens: Results depend on environment defaults.
An explicit encoding makes the data contract portable.