_csv.Error: line contains NUL (tested on Python 3.13)

_csv.Error: line contains NUL means a \x00 byte reached csv.reader. On Python 3.11 and newer you won't even see that error: we measured that the NUL now ends up inside your field values with no warning. Here is how to tell a UTF-16 file from a stray byte, and the fix for each.

1. Where the error went

_csv.Error: line contains NUL (older wording: line contains NULL byte) is what csv.reader raised on Python 3.10 and earlier when a line held a \x00 byte. CPython 3.11.0a2 changed that (bpo-27580, changelog entry: “Add support of null characters in csv”). We tested on Python 3.13.5 only, so the old message above is from the changelog and the many existing reports of it, not our own run. What we did measure is what happens now:

import csv

with open("data.csv", "wb") as f:
    f.write(b"name,qty\nwidget\x00,3\n")

with open("data.csv", newline="") as f:
    rows = list(csv.reader(f))
print(rows)
print(rows[1][0] == "widget")
[['name', 'qty'], ['widget\x00', '3']]
False

Measured: no error at all. The NUL byte is kept inside the field, so "widget\x00" is not equal to "widget". Lookups, joins, de-duplication and database inserts that compare on that column now fail quietly instead of loudly. If you upgraded from 3.10 and the error “went away”, the bad bytes are still in your data.

2. The usual real cause: a UTF-16 file

A file with a NUL in almost every other byte is rarely corrupt. It is usually UTF-16 text: Excel's “Unicode Text” save option, PowerShell 5's Out-File/> redirection, and many Windows exports write UTF-16, where every ASCII character is followed by a \x00.

import csv

# what Excel's "Unicode Text" export (and many Windows tools) produce
with open("export.csv", "w", encoding="utf-16", newline="") as f:
    f.write("name,qty\r\nwidget,3\r\n")

print(open("export.csv", "rb").read()[:10])

with open("export.csv", newline="", encoding="latin-1") as f:
    print(list(csv.reader(f))[:2])
b'\xff\xfen\x00a\x00m\x00e\x00'
[['ÿþn\x00a\x00m\x00e\x00', '\x00q\x00t\x00y\x00'], ['\x00']]

Measured: the file starts with b'\xff\xfe' (the UTF-16 little-endian byte order mark). Decoded as a single-byte encoding, every value comes back padded with NULs and extra rows of just '\x00' appear between the real ones, with no exception. With Python's default UTF-8 decoding you get a different error instead: UnicodeDecodeError: 'utf-8' codec can't decode byte 0xff in position 0: invalid start byte (see our UnicodeDecodeError guide). On Python 3.10 and older, the latin-1 / errors="ignore" route is exactly where “line contains NUL” came from.

3. Fix: open it with the right encoding

import csv

def open_csv(path):
    with open(path, "rb") as f:
        head = f.read(4)
    if head.startswith((b"\xff\xfe", b"\xfe\xff")):
        enc = "utf-16"
    elif head.startswith(b"\xef\xbb\xbf"):
        enc = "utf-8-sig"
    else:
        enc = "utf-8"
    return open(path, newline="", encoding=enc)

with open_csv("export.csv") as f:
    print(list(csv.reader(f)))
with open_csv("data.csv") as f:
    print(list(csv.reader(f)))
[['name', 'qty'], ['widget', '3']]
[['name', 'qty'], ['widget\x00', '3']]

Measured: checking the first bytes for a byte order mark and opening with encoding="utf-16" gives clean rows. "utf-16" reads the BOM itself and strips it, so you don't need to know the byte order. The second file has no BOM; it is UTF-8 with a stray NUL, so the encoding fix correctly leaves it alone. That case needs the next step.

4. Fix: strip stray NUL bytes

import csv

with open("data.csv", newline="") as f:
    rows = list(csv.reader(line.replace("\0", "") for line in f))
print(rows)
[['name', 'qty'], ['widget', '3']]

Measured: csv.reader accepts any iterable of strings, so a generator that removes "\0" from each line works on every Python version, old and new. Only do this after ruling out UTF-16 (section 3); stripping NULs from UTF-16 text decoded as a single-byte encoding leaves the BOM characters and stray rows behind.

5. Find the bytes first

with open("data.csv", "rb") as f:
    data = f.read()
print(data.count(b"\x00"), "NUL bytes, first at offset", data.find(b"\x00"))
1 NUL bytes, first at offset 15

One or a few NULs at random offsets points to a truncated write, a fixed-width field padded with zeros, or a binary column dumped as text: strip them. NULs at every other byte points to UTF-16: re-open it. A file that is mostly NULs is probably not CSV at all (a zero-filled file after a crash, or a .xlsx renamed to .csv).

Frequently asked questions

Why don't I get 'line contains NUL' any more?

CPython 3.11 added NUL support to the csv module (bpo-27580, in 3.11.0a2). We measured on Python 3.13.5 that csv.reader now returns the NUL inside the field ('widget\x00') with no error, so the data is still dirty but nothing tells you.

What does 'line contains NUL' mean?

A line in the file contains a \x00 byte. Most often the file is UTF-16 (Excel 'Unicode Text', PowerShell 5 redirection), which we confirmed starts with b'\xff\xfe' and has a NUL after every ASCII character; less often it is a stray NUL from a truncated or padded write.

How do I read a UTF-16 CSV in Python?

Open it with open(path, newline='', encoding='utf-16'). We measured that this reads the byte order mark, strips it, and returns clean rows like ['name', 'qty'].

How do I remove NUL bytes before csv.reader?

Pass a generator: csv.reader(line.replace('\0', '') for line in f). We confirmed it turns ['widget\x00', '3'] into ['widget', '3'] and it works on every Python version.

Does errors='ignore' fix it?

No. It hides the decoding error but the NUL bytes stay in the text; on Python 3.10 and older that is exactly the path that leads to line contains NUL, and on 3.11+ you get NUL-padded fields with no error.

Written with AI assistance. Measured on 2026-10-09 with Python 3.13.5's built-in csv module: wrote a UTF-8 file with one stray \x00 byte and a UTF-16 file the way Excel's Unicode Text export does, read each with csv.reader under utf-8, latin-1 and utf-16, and ran every code block on this page verbatim. Python 3.10 behavior is from CPython's changelog (bpo-27580), not our own run. PlainHeaders is our extension; Python's csv module is built in.