2025, Dec 07 03:00

How to fix newline bugs in Python: normalize line endings, use write vs writelines, and append mode

Learn why tasks merge onto one line in a Python task manager and how to fix it: normalize newlines, use write vs writelines, and append mode for efficient I/O.

When building a simple task manager in Python, it is easy to overlook how strings are written to disk. A small refactor from match-case to if-else introduced an output issue: newly added tasks started sticking to the end of the previous line instead of appearing on their own line. The core of the problem is how newline characters are handled during read and write operations.

Problem demonstration

The following snippet captures the behavior where added tasks get concatenated because they are written without a terminating newline:

while True:
    cmd = input("Type add, show, edit, complete or exit: ")
    cmd = cmd.strip()
    if 'add' in cmd or 'new' in cmd:
        entry = cmd[4:]
        with open('tasks.txt', 'r') as fh:
            lines = fh.readlines()
        lines.append(entry)
        with open('tasks.txt', 'w') as fh:
            fh.writelines(lines)
    elif 'show' in cmd:
        with open('tasks.txt', 'r') as fh:
            lines = fh.readlines()
        for idx, val in enumerate(lines):
            val = val.strip('\n')
            row = f"{idx+1}-{val}"
            print(row)

What went wrong

The issue likely appeared after adding cmd = cmd.strip(), which removes the newline \n at the end of the input. The function writelines() does not append newline characters for you; it writes the strings exactly as they appear in the list. Meanwhile, strings read via readlines() already include a trailing \n. When a new task is appended without that newline and the entire list is flushed with writelines(), the last existing line and the new task end up on a single line.

The fix

Ensure that every task is written with a newline. Before writing the list back to disk, normalize each string so it ends with a single \n. This prevents line joining regardless of prior formatting:

with open('tasks.txt', 'w') as fh:
    for line in lines:
        line = line.strip('\n')
        fh.write(line + '\n')

If the file might not exist yet, guard the initial read to avoid an exception and start with an empty collection instead:

try:
    with open('tasks.txt', 'r') as fh:
        lines = fh.readlines()
except FileNotFoundError:
    lines = []

When the goal is to add a task to an existing file, consider open() with mode 'a' so you do not need to read all lines and rewrite the file for each insertion.

Why this matters

Text processing in Python is deceptively simple, yet newline handling is a frequent source of subtle bugs. Misunderstanding how readlines(), writelines(), and string termination interact leads to malformed data, confusing output, and unnecessary I/O. By ensuring consistent line endings and choosing the appropriate file mode, you keep data clean and operations efficient.

Conclusion

Keep three practical habits. Always normalize line endings before writing, especially when mixing strings from input and readlines(). Use writelines() only when your list elements already include the required \n, or switch to write() in a loop that enforces it. When you merely need to add entries, look at mode 'a' to append without rewriting the entire file. These small choices prevent concatenated lines and save time during later maintenance.