Перейти к содержанию
Learning Platform
Глоссарий Troubleshooting
Урок 10.01 · 22 мин
Средний
File I/Oopen()encodingUTF-8Latin-1BOMnewlinewith-statementio.StringIOio.TextIOWrapperchunkinggenerator

Text I/O fundamentals: encoding, newlines, io.StringIO

Это первый урок Module 09 — введение в file I/O fundamentals. Production data engineer ежедневно читает и пишет CSV / JSON / log files; ошибки в encoding / newline handling / file lifecycle — массовый источник production bugs. М09 строится pragmatic-DEEP: stdlib API + production pitfalls + cross-links к Storage Formats / Spark / ClickHouse.

Главный invariant Module 09 (carrying milestone-level constraint): в browser challenges мы НЕ читаем реальные файлы. Pyodide MEMFS — in-memory filesystem; реальный disk учащегося browser не видит. Мы используем io.StringIO simulation pattern — оборачиваем строку в file-like объект. Реальный open() появляется только в lesson prose (Markdown rendered as syntax-highlighted text — не выполняется) и в Run-on-Your-Machine callouts (lesson 06).

В этом уроке:

  1. Why text I/O — production контекст.
  2. open() modesr/w/a/x/+/b table.
  3. Encoding — UTF-8 (default) / Latin-1 / UTF-16-BOM; errors='replace'/'ignore'.
  4. Newline handlingnewline='' для CSV (Pitfall — Windows \r\n translation).
  5. with-statement — context manager recipe — cross-link M06 урок 04.
  6. io.StringIO simulation — Pattern 1 setup для challenges 02/03.
  7. Chunking via generatordef read_chunks(buf, size): yield buf.read(size)cross-link M05 урок 02.
  8. Encoding pitfalls — BOM detection, mojibake, surrogate pairs.

Why text I/O — production контекст

Вы читаете CSV из vendor’а, JSON из API response cache, log files после rotation. Каждое из этих действий проходит через text I/O layeropen() + encoding + newline handling. Ошибки на этом layer:

  • Mojibake'café' превращается в 'café' потому что байты 0xC3 0xA9 (UTF-8 для é) интерпретированы как Latin-1.
  • UnicodeDecodeError — file объявлен encoding='utf-8', но содержит Windows-1251 байты от legacy export.
  • csv.Error: new-line character seen in unquoted field — забыли newline='' при чтении CSV на Windows.
  • ResourceWarning: unclosed file — забыли with-statement, file descriptor leak, под нагрузкой OS bound на open files.

Pragmatic-DEEP rule: каждый production-grade file read должен explicit declare encoding и использовать with-statement. Default encoding (locale.getpreferredencoding()) — platform-specific и не портативен между dev / CI / production.


open() modes — r/w/a/x/+/b table

Сигнатура: open(file, mode='r', buffering=-1, encoding=None, errors=None, newline=None). Mode — combinable string:

ModeReadWriteTruncatePositionUse case
'r'данетstartdefault — read existing file
'w'нетдада (overwrite)startwrite new file (creates если absent)
'a'нетданетendappend (creates если absent)
'x'нетдаstartexclusive createFileExistsError если exists
'r+'даданетstartread + write same file (advanced)
'w+'дададаstarttruncate + read + write (rare)
'a+'даданетendappend + occasional read

Binary mode — добавь b: 'rb', 'wb', 'ab'. В binary mode возвращает bytes, не str. Используется для:

  • compression (gzip, bz2, lzma) — урок 05;
  • binary formats (Parquet, ORC, Avro, Arrow IPC) — урок 04;
  • raw bytes parsing (image / video / network protocol headers).

В text mode (без b) возвращает str — Python автоматически decode’ит байты согласно encoding parameter.

# Text mode — UTF-8 explicit (production-grade)
with open('users.csv', 'r', encoding='utf-8') as f:
    content = f.read()  # type: str

# Binary mode — без encoding
with open('image.png', 'rb') as f:
    raw = f.read()  # type: bytes

# Exclusive create — fails if file exists
with open('config.lock', 'x') as f:
    f.write('locked')

Cite docs.python.org/3/library/functions.html#open — full mode table + buffering semantics.


Encoding — UTF-8, Latin-1, UTF-16-BOM

UTF-8 — default Python 3 source encoding (PEP 3120) и dominant standard для text exchange. Variable-width: 1-4 байта на codepoint. ASCII-compatible (ASCII codepoints занимают 1 byte и идентичны pure ASCII).

Latin-1 (a.k.a. ISO-8859-1) — single-byte encoding для Western European symbols. Legacy databases / banking exports / pre-Unicode systems. Не Unicode-complete (не хватает é в proper form, не хватает Cyrillic / CJK).

UTF-16-BOM — Windows artifact. Notepad по default writes UTF-16 LE c BOM 0xFF 0xFE. Excel exports CSV в UTF-16 на Windows. Python автоматически detect’ит BOM если encoding declared 'utf-16', но не если declared 'utf-8' — вы получите  BOM character как первый “символ” file.

Recipe — production-grade UTF-8 read с error handling:

def read_text_safe(path: str, encoding: str = 'utf-8') -> str:
    """Read text file with explicit encoding + error reporting."""
    try:
        with open(path, 'r', encoding=encoding) as f:
            return f.read()
    except UnicodeDecodeError as e:
        # Файл объявлен utf-8 но содержит non-UTF-8 байты
        raise ValueError(
            f'{path}: not valid {encoding} (byte {e.start}-{e.end}: {e.reason})'
        ) from e

errors= parameter — strategy для invalid bytes:

ValueBehavior
'strict' (default)raise UnicodeDecodeError — fail loud
'replace'invalid bytes → '�' (replacement char ?) — survive bad data
'ignore'drop invalid bytes silently — dangerous (data loss without trace)
'backslashreplace'\xff'\\xff' literal — debugging
'surrogateescape'invalid bytes → low-surrogate codepoints — lossless round-trip

Production rule: 'strict' для validation pipelines (catch bad data early); 'replace' для display / log ingestion (don’t crash on legacy junk); никогда 'ignore' без logging — silent data loss.

# Survive mixed-encoding log files
with open('legacy.log', 'r', encoding='utf-8', errors='replace') as f:
    for line in f:
        print(line.rstrip())  # invalid bytes → ?

Cite docs.python.org/3/library/codecs.html#error-handlers.


Newline handling — newline='' for CSV

Параметр newline=subtle но production-critical. Default (newline=None) включает universal newlines mode\r\n, \r, \n все трансформируются в \n при чтении и \nos.linesep при записи. Удобно для прозы, но:

# WRONG для CSV — universal newlines делает встроенные \r\n внутри quoted field translated
import csv
with open('windows.csv', 'r') as f:  # ← newline=None default
    reader = csv.reader(f)
    for row in reader:
        print(row)  # _csv.Error: new-line character seen in unquoted field

Правильноnewline='' для CSV (caller перекладывает newline handling на csv module):

import csv
with open('windows.csv', 'r', encoding='utf-8', newline='') as f:
    reader = csv.reader(f)
    for row in reader:
        print(row)  # OK

Cite docs.python.org/3/library/csv.html#footnotes — explicit warning о newline=''.


Без with-statement file descriptor leak — anti-pattern:

# ANTI-PATTERN — leaks descriptor если read() raises
f = open('data.csv', 'r')
content = f.read()  # ← exception здесь = no f.close()
f.close()

С with-statement — гарантированный cleanup даже при exception:

# Production — context manager guarantees close
with open('data.csv', 'r', encoding='utf-8') as f:
    content = f.read()
# f автоматически closed — даже если read() raises

Cross-link M06 урок 04 (context-manager protocol): open() возвращает io.TextIOWrapper instance, который реализует __enter__ / __exit__ методы. with statement — syntactic sugar над:

f = open('data.csv', 'r', encoding='utf-8')
try:
    content = f.read()
finally:
    f.close()  # ← __exit__ внутри context manager делает то же самое

Recap М06 урок 04 protocol foundation: любой объект с __enter__ + __exit__ — context manager. open() возвращает TextIOWrapper/BufferedReader/FileIO (depending on mode), всё реализует protocol. Это architectural reuse — same pattern что мы выучили для locks, transactions, temp files.

Cite docs.python.org/3/library/io.html#io.TextIOWrapper.


io.StringIO simulation — Pattern 1 setup

В browser challenges Module 09 мы не можем open реальный файл. Pyodide MEMFS — in-memory FS, пустой по default; реальный disk учащегося недоступен (security boundary). Решение: simulate file через io.StringIO:

import io
import csv

# Simulated CSV file — string wrapped в file-like object
csv_data = "name,age,role\nalice,30,dev\nbob,25,qa\n"
buf = io.StringIO(csv_data)

reader = csv.DictReader(buf)
for row in reader:
    print(row)  # {'name': 'alice', 'age': '30', 'role': 'dev'}
                # {'name': 'bob',   'age': '25', 'role': 'qa'}

Что предоставляет io.StringIO:

  • file-like API: .read(), .readline(), .readlines(), .write(...), .seek(0), .tell()
  • iterability: for line in buf: — line-by-line как настоящий file
  • in-memory storage: ничего не записывается на disk
  • type: io.StringIO для str; io.BytesIO для bytes (binary mode equivalent — урок 05)

Production usage: io.StringIO ценен и вне Pyodide — для unit tests (parse строки without temp file), для in-memory pipe между producer и consumer, для capture stdout contextlib.redirect_stdout(buf).

В уроке 02 (CSV) — Pattern 1 challenge py-m09-02-code-1 показывает full canonical use:

import io
import csv

def solve(csv_str: str) -> list[dict]:
    buf = io.StringIO(csv_str)
    reader = csv.DictReader(buf)
    rows = []
    for row in reader:
        row['age'] = int(row['age'])  # csv возвращает str — конвертация — caller's responsibility
        rows.append(row)
    return rows

Cite docs.python.org/3/library/io.html#io.StringIO.


Для больших файлов (>RAM) f.read() загружает всё в память — OOM. Решение — chunked reading через generator:

import io
from collections.abc import Iterator

def read_chunks(buf: io.IOBase, size: int = 1024) -> Iterator[str]:
    """Yield chunks of `size` chars until EOF."""
    while chunk := buf.read(size):
        yield chunk

Как это работает:

  • walrus := (Python 3.8+) присваивает + проверяет в одной expression;
  • while chunk: ... exits когда f.read(size) returns empty string (EOF — convention для text I/O);
  • yield chunklazy evaluation — caller получает chunk по требованию.

Cross-link M05 урок 02 (generator climax): generator function — функция с yield. CPython создаёт PyGenObject instance — он сохраняет local state (locals + bytecode pointer) между вызовами next(). Это позволяет process файл размером 100GB используя constant memory — только текущий chunk в RAM.

Recipe — process large CSV без OOM:

def total_bytes(path: str) -> int:
    """Count total bytes in file без loading в память."""
    with open(path, 'rb') as f:
        return sum(len(chunk) for chunk in read_chunks(f, size=1 << 16))
                                                           # ↑ 64KB chunks

В уроке 03 (JSON streaming) увидим тот же pattern для JSONL — for line in f: json.loads(line) — generator-based streaming.

Cite PEP 525 — Asynchronous Generators и М05 урок 02 PyGenObject.


Encoding pitfalls — BOM, mojibake, surrogate pairs

BOM (Byte Order Mark)

UTF-8-BOM начинается с байт 0xEF 0xBB 0xBF. Microsoft Notepad / Excel insert это даже когда файл — UTF-8 (BOM не нужен в UTF-8 — byte order не имеет смысла для 1-byte units, но Microsoft tools insert anyway). Эффект:

with open('excel-export.csv', 'r', encoding='utf-8') as f:
    print(repr(f.readline()))  # 'name,age,role\n'
                                #  ↑ BOM как первый "character"

Решение: explicit encoding='utf-8-sig' — strips BOM при чтении:

with open('excel-export.csv', 'r', encoding='utf-8-sig') as f:
    print(repr(f.readline()))  # 'name,age,role\n' — BOM stripped

Mojibake

Файл написан UTF-8 ('é' = 0xC3 0xA9), но открыт с encoding=‘latin-1’ — каждый byte интерпретируется отдельно: 0xC3'Ã', 0xA9'©'. Result: 'café'. Это mojibake — visually corrupted text, но bytes неповрежденные. Reverse fix: re-encode latin-1 → bytes → decode utf-8.

broken = 'café'                                  # mojibake
fixed = broken.encode('latin-1').decode('utf-8')  # 'café'

Surrogate pairs

Symbols вне BMP (Unicode codepoint > U+FFFF — emoji, ancient scripts, math) занимают 4 bytes в UTF-8 и 2 code units в UTF-16. Surrogate pairs — UTF-16 mechanism для кодирования таких codepoints. Python str использует internal representation согласно PEP 393 — flexible width per-string, не UTF-16 surrogates. len('🐍') == 1 — 1 codepoint, не 2.

Production rule: всегда работайте с codepoints (str), не с UTF-16 code units. Если interfacing с JS / Java / Windows API — используйте 'utf-16' encoding в open() или surrogateescape error handler для round-trip preservation.


Что в следующем уроке

Урок 02 — CSV stdlib API (csv.reader, csv.DictReader, dialects, quoting). Code-challenge py-m09-02-code-1 — Pattern 1 (CSV via io.StringIO + csv.DictReader). Cross-course → ClickHouse FORMAT CSV clause + Spark schema inference. Foundation, заложенная в этом уроке (io.StringIO, with-statement, encoding=‘utf-8’), переносится напрямую.

Pragmatic-DEEP принцип: не ULTRA-DEEP’аем кодеки. UTF-8 / Latin-1 / UTF-16 — production necessity, остальные encodings (KOI8-R, Big5, Shift-JIS) — деталь legacy migration; learn on demand.

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 7