open() Default Encoding Trap: The Config Loader That Works on Dev and Explodes in Docker2026-09-01
This function loads a newline-delimited JSON file of users. It works flawlessly on the developer's laptop, passes CI, and ships to production. Three hours after deploy, a customer named François signs up and the entire onboarding worker dies.
import json
def load_users(path):
"""Read a JSONL file of user records."""
users = []
with open(path) as f:
for line in f:
users.append(json.loads(line))
return users
def greet_all(path):
for user in load_users(path):
print(f"Hello, {user['name']}!")
if __name__ == "__main__":
greet_all("users.jsonl")
# UnicodeDecodeError: 'ascii' codec can't decode byte 0xc3
# in position 17: ordinal not in range(128)
Python's open() in text mode does not default to UTF-8. It defaults to whatever locale.getpreferredencoding(False) returns for the current process. On the developer's Mac or Ubuntu machine, that's almost always UTF-8. Inside a minimal Docker image — python:3.11-slim, alpine, distroless, anything without a full locale package — the C library reports the locale as C or POSIX, and the preferred encoding becomes ASCII.
So the same code path does two different things:
open("users.jsonl") → UTF-8 decoder → "François" reads fine.open("users.jsonl") → ASCII decoder → the first byte of ç (0xC3) is outside 0–127 → UnicodeDecodeError, mid-file, on an innocent-looking for line in f.The failure mode is nasty because it depends on data, not code. Every ASCII-only user works. The bug hides behind whichever test fixture your CI happens to generate. It surfaces the moment a real customer types a character above U+007F — and by then you're staring at a traceback pointing at a line that "obviously" just reads a file.
Windows adds its own flavor: the default there is often cp1252, which silently mis-decodes UTF-8 bytes into mojibake instead of raising. That's arguably worse — no exception, just corrupted names in your database.
Always pass encoding= explicitly when opening text files. Never rely on the locale.
def load_users(path):
users = []
with open(path, encoding="utf-8") as f:
for line in f:
users.append(json.loads(line))
return users
Additional defenses worth knowing:
EncodingWarning with -X warn_default_encoding or PYTHONWARNDEFAULTENCODING=1. Turn this on in CI — it flags every open() missing an encoding=."rb" mode — the encoding question doesn't apply and can't bite you./etc/passwd), pass encoding=locale.getencoding() so the intent is visible in the diff.The rule is simple: text mode without an explicit encoding is a bug waiting for a customer whose name has an accent.
open() in text mode uses the locale's preferred encoding, not UTF-8 — always pass encoding="utf-8" so your code doesn't depend on whether the container image happened to install a locale.
