Python splitlines() breaks JSONL: the culprit was U+2028, an invisible line separator

日本語版

Development log of the Anime Type Quiz / October 2026

While building the data for the Anime Type Quiz, I fixed a bug where the JSON broke. The cause was splitlines(), a perfectly ordinary function.

What broke

The quiz data comes from Filmarks and is stored in data/filmarks_works.jsonl. JSONL puts one JSON object per line, and you read it by splitting into lines and passing each one to json.loads. In Python I had first written:

for line in path.read_text(encoding='utf-8').splitlines():
    r = json.loads(line)

But when a synopsis contained U+2028 (LINE SEPARATOR), that line was split in the middle, and json.loads was handed JSON that ended inside a string, and failed.

What U+2028 is

U+2028 is a Unicode character meaning “line separator.” It looks almost like a newline. In JSON, it is valid to put it raw inside a string, and the writing side (json.dumps(..., ensure_ascii=False)) outputs it as is.

The problem was on the reading side. Python’s str.splitlines() splits not only on \n but also on Unicode line boundaries such as U+2028. split('\n') splits only on \n.

s = json.dumps({"a": "x\u2028y"}, ensure_ascii=False)
len(s.splitlines())   # 2  (one line splits in two)
len(s.split('\n'))    # 1

Passing each half to json.loads raises Unterminated string. The short example above reproduces exactly that.

The numbers in the real data

I recounted both ways on the current data file.

How it is splitNumber of lines
split('\n') (real newlines only)7,247
splitlines()7,255

The difference is 8. Counting U+2028 in the file gave exactly 8, in the lines of 2 works. Two works out of 7,247, under 0.03%, were enough to make the whole file unreadable.

The fix

One line:

- for line in path.read_text(encoding='utf-8').splitlines():
+ for line in path.read_text(encoding='utf-8').split('\n'):

I left the reason in a comment so that my future self would not be puzzled: when making JSONL, the only line separator is \n, so reading should split on \n only.

Why it is hard to notice

One more fix from the same day

The same commit fixed another bug caused by characters that look alike but are different. When matching titles, Annict’s <物語> (full-width angle brackets) and Filmarks’s 〈物語〉 (Japanese angle brackets) did not match even though they were the same work. NFKC normalization turns the full-width <> into half-width, but 〈〉 stays as it is, so I removed both from the comparison string.

Both are the same lesson: characters are not always what they look like.

What I learned

A function I used “for newlines” had a wider idea of newlines than I assumed.

When reading JSONL, do not use splitlines(). Split on the character the writer used as the separator (\n), or use for line in f:, which splits only on \n. I am writing it here for the next time the data grows.

The numbers in this article come from recounting the data file on October 7, 2026.

This is the English version of the original Japanese article.

More in English