UnicodeEncodeError: 'ascii' codec can't encode character
1 · The lesson
readWhat this error means
You are writing text out — to a file, a terminal, a socket — and the encoding in use has no way to represent one of the characters. ASCII can encode 128 characters; ₹, é and → are not among them.
This is the mirror of UnicodeDecodeError. Decoding is bytes coming in; encoding is text going out.
When you see it
Traceback (most recent call last): File "export.py", line 9, in <module> f.write(f"Total: ₹{amount}") UnicodeEncodeError: 'ascii' codec can't encode character '₹' in position 7: ordinal not in range(128)
On Windows the codec is usually different, the cause identical:
UnicodeEncodeError: 'charmap' codec can't encode character '→' in position 12: character maps to <undefined>
Why it happens
open() uses the platform's preferred encoding when you do not name one. That is UTF-8 on modern Linux and macOS, but in a container with no locale it can be ASCII, and on Windows it has historically been a legacy code page such as cp1252.
So the same code writes a rupee sign happily on a laptop and fails in the container, which is why this so often appears only after a deploy.
How to fix it
Name the encoding. Every time you open a file.
with open("report.txt", "w", encoding="utf-8") as f: f.write(f"Total: ₹{amount}")
setup added so this can run · defines amount
# Lightweight mock for objects whose attributes/methods aren't critical class _AutoMock: def __init__(self, name='mock'): self._name = name def __getattr__(self, k): return _AutoMock(self._name + '.' + k) def __call__(self, *a, **kw): print('-> ' + self._name + '() called') return _AutoMock(self._name + '()') def __repr__(self): return '<mock ' + self._name + '>' def __str__(self): return '<mock ' + self._name + '>' def __bool__(self): return True def __iter__(self): return iter([]) def __len__(self): return 0 def __getitem__(self, k): return _AutoMock(self._name + '[...]') def __setitem__(self, k, v): pass def __enter__(self): return self def __exit__(self, *a): return False async def __aenter__(self): return self async def __aexit__(self, *a): return False def __add__(self, o): return self def __radd__(self, o): return self def __sub__(self, o): return self def __mul__(self, o): return self def __rmul__(self, o): return self def __truediv__(self, o): return self def __eq__(self, o): return isinstance(o, _AutoMock) def __hash__(self): return hash(self._name) def __lt__(self, o): return True def __le__(self, o): return True def __gt__(self, o): return False def __ge__(self, o): return False def __mro_entries__(self, bases): return (object,) amount = _AutoMock('amount')
There is no good reason to rely on the platform default. Writing encoding="utf-8" is the whole fix and it makes the code behave the same everywhere.
For stdout in a container, set the environment rather than the code:
ENV PYTHONIOENCODING=utf-8 ENV LANG=C.UTF-8
Python 3.15 makes UTF-8 mode the default, which removes most of this class of problem — but code that names its encoding is correct on every version.
When the destination genuinely cannot take the character, choose what to lose, explicitly:
text = "Total: ₹1,200" text.encode("ascii", errors="ignore") # b'Total: 1,200' — drops it silently text.encode("ascii", errors="replace") # b'Total: ?1,200' — marks the gap text.encode("ascii", errors="xmlcharrefreplace") # b'Total: ₹1,200'
Prefer replace over ignore in anything you will read later: a ? tells you something was lost, whereas ignore produces text that looks correct and is quietly wrong.
For CSV that Excel will open, use utf-8-sig. Excel assumes a legacy code page unless the file begins with a byte-order mark, and this is why exported CSVs so often show ₹ where the rupee sign should be:
with open("export.csv", "w", encoding="utf-8-sig", newline="") as f: csv.writer(f).writerows(rows)
setup added so this can run · defines rows, csv
# Lightweight mock for objects whose attributes/methods aren't critical class _AutoMock: def __init__(self, name='mock'): self._name = name def __getattr__(self, k): return _AutoMock(self._name + '.' + k) def __call__(self, *a, **kw): print('-> ' + self._name + '() called') return _AutoMock(self._name + '()') def __repr__(self): return '<mock ' + self._name + '>' def __str__(self): return '<mock ' + self._name + '>' def __bool__(self): return True def __iter__(self): return iter([]) def __len__(self): return 0 def __getitem__(self, k): return _AutoMock(self._name + '[...]') def __setitem__(self, k, v): pass def __enter__(self): return self def __exit__(self, *a): return False async def __aenter__(self): return self async def __aexit__(self, *a): return False def __add__(self, o): return self def __radd__(self, o): return self def __sub__(self, o): return self def __mul__(self, o): return self def __rmul__(self, o): return self def __truediv__(self, o): return self def __eq__(self, o): return isinstance(o, _AutoMock) def __hash__(self): return hash(self._name) def __lt__(self, o): return True def __le__(self, o): return True def __gt__(self, o): return False def __ge__(self, o): return False def __mro_entries__(self, bases): return (object,) rows = _AutoMock('rows') csv = _AutoMock('csv')
When you'd actually see this in real code
- A report containing a customer name with an accent, generated fine locally and failing in the nightly container.
print()of a log line with an arrow or emoji, on a Windows console using cp1252.- A CSV of Indian prices opening in Excel as mojibake — the same problem, one layer along.
- Any code that writes user-supplied text without naming an encoding. It works until the first user whose name is not ASCII.
Related errors
- UnicodeDecodeError: 'utf-8' codec can't decode byte — the same problem in the other direction, reading rather than writing.
See Also
- All Python errors — the full index, by type and by when it happens.
- Working with Strings
- File I/O, Pathlib, and Large Files
- CSV & JSON