Python з нуля · Блок 5 · Тема 20

Файли й pathlib

Програма забуває все, щойно завершилась. Диск — це її памʼять на завтра, і розмова з диском має свої правила: режим відкриття, кодування, гарантоване закриття. Порушиш одне — і український текст перетвориться на мотлох, а прайс за півроку зникне від одного зайвого символу.

Усі дані, з якими ми досі працювали, ми набирали руками. Список покупок у темі 06, словник цін у темі 08, зіпсовані рядки прайсу в темі 18 — усе це літерали, записані просто в коді. Порахували, надрукували, закрили вікно. Наступного разу все починається з тих самих літералів, бо більше нічого й не лишилось.

Це нормально, поки ти вчишся. Але жодна справжня програма так не працює: дані приходять ззовні й мають пережити завершення програми. Прайс вивантажили з бухгалтерії, результат треба віддати колезі, журнал подій має існувати завтра вранці. Усе це — файли, і сьогоднішня тема відкриває блок «Робота з реальністю»: там, де дані вже не ми вигадуємо.

Наскрізний приклад лишається той самий, що в темі 18, — прайс із трьох рядків. Тільки тепер він не змінна в коді, а файл prays.txt на диску:

хліб;28.5 молоко;32.0 яблука;19.0

01 / МотивЧому програма все забуває

Коли ти запускаєш скрипт, операційна система виділяє йому шматок оперативної памʼяті. Там живуть усі обʼєкти: числа, рядки, списки, словники. Коли скрипт завершується, система забирає цей шматок назад і віддає наступній програмі. Нічого не «зберігається саме собою»: памʼять для того й потрібна, щоб її переприділяти.

Побутова аналогія проста. Оперативна памʼять — це стіл: широкий, швидкий, але ввечері прибиральниця змітає з нього все. Диск — це шафа: повільніша, зате вранці все на місці.

Файл — це названа послідовність байтів, яку зберігає операційна система. Байт — число від 0 до 255, і сама система не знає, що ті байти означають: текст прайсу, фотографію чи музику. Тлумачення — цілком наша справа, і саме звідси візьметься половина сьогоднішніх пасток.

У файлів є друга роль, не менш важлива: це спосіб обміну між програмами. Бухгалтерка вивантажила таблицю, ти прочитав її Python-ом, записав результат, вона відкрила його в Excel. Жодна з трьох програм нічого не знає про дві інші — домовились лише про формат файлу.

Робота з файлом складається з трьох дій: відкрити, прочитати або записати, закрити. Найважча — остання, бо про неї забувають, а наслідки видно не одразу.

02 / Основаopen(): файл як обʼєкт

Уся робота починається з однієї вбудованої функції:

f = open("prays.txt", "r", encoding="utf-8") print(f) <_io.TextIOWrapper name='prays.txt' mode='r' encoding='utf-8'>

Зверни увагу на те, чого тут немає: вмісту файлу. open не читає нічого — вона повертає обʼєкт, через який читати можна. Це принципово: якби open одразу читала файл, відкрити фільм на чотири гігабайти було б неможливо.

Точніша аналогія — не «прочитати книжку», а «покласти закладку». Файловий обʼєкт памʼятає дві речі: який файл відкрито і на якій позиції ми зараз стоїмо. Кожне читання зсуває позицію вперед.

Аргументів у open три: шлях (рядок або обʼєкт Path із розділу 08), режим — що саме ми збираємось робити, за замовчуванням "r", тобто «читати», — і encoding, тобто за якою таблицею перекладати байти в символи. Останній формально не обовʼязковий, а фактично обовʼязковий завжди: чому — у розділі 05.

Текстовий режим і двійковий

Літера b в режимі ("rb", "wb") перемикає файл у двійковий режим, і різниця більша, ніж здається:

текстовий ("r")двійковий ("rb")
що віддаєstr — символиbytes — числа 0…255
кодуваннядекодує за encodingне чіпає взагалі
кінці рядківбудь-які перетворює на \nлишає як є
для чоготекст, CSV, JSON, кодкартинки, архіви, бази, мережа

Правило вибору без винятків: текст для людини — текстовий режим з encoding, усе інше — двійковий. Спроба відкрити картинку в текстовому режимі майже напевно дасть UnicodeDecodeError, і це ще щасливий кінець.

03 / НаміриРежими r, w, a, x

Другий аргумент open — це оголошення намірів. Ти кажеш системі, що збираєшся робити, а вона на основі цього вирішує, чи створювати файл і що зробити з наявним вмістом. Чотири базові літери:

режимфайлу немаєфайл уже єдозволяє
rFileNotFoundErrorвідкрити з початкулише читати
wстворити порожнійстерти вмістлише писати
aстворити порожнійстати в кінецьлише дописувати
xстворити порожнійFileExistsErrorлише писати

Найважливіший рядок таблиці — другий. "w" стирає файл у мить відкриття, ще до першого write. Не після, не «якщо запис вдався» — одразу. Рядок open("prays.txt", "w"), виконаний випадково, знищує прайс навіть тоді, коли наступний рядок програми падає з винятком. Відновити немає звідки.

Інтерактив 1 · Що робить кожен режим із твоїм файлом

Обери режим і те, чи файл уже існує. Дивись на середню колонку: вона показує стан одразу після open(), ще до запису.

рядків до open
рядків після open
рядків після запису
курсор на
Прогони w на наявному файлі й подивись на середню колонку. Було три рядки — стало нуль, і жодного попередження. Порівняй із x: той самий намір, але замість тихого знищення — FileExistsError і жоден байт не втрачено.

Ще дві деталі. "a" пише тільки в кінець: навіть якщо перемотати позицію на початок, наступний write усе одно опиниться у хвості — так працює прапорець O_APPEND в операційній системі. І плюс відкриває друге вміння: "r+" — читати й писати в наявному файлі, "w+" — писати й читати, попередньо стерши. Новачкові вони не потрібні майже ніколи, і це добре: змішувати читання із записом в одному обʼєкті — надійний спосіб заплутатись у позиції.

04 / Гарантіяwith: обіцянка з теми 18

У темі 18 ми домовились, що with — це try/finally, спакований у два слова, і що докладно буде тут. Час виконувати обіцянку.

Чому взагалі треба закривати файл? Три причини, і всі практичні:

  1. Дані не на диску, доки файл не закритий. Запис іде через буфер у памʼяті — так швидше. close() скидає буфер на диск.
  2. Файловий дескриптор — обмежений ресурс. Система дає процесу скінченну кількість відкритих файлів (типово тисячу-дві), і цикл, що відкриває й не закриває, впаде з OSError: Too many open files.
  3. Файл заблокований. У Windows відкритий файл часто не можна ані перейменувати, ані видалити — і незрозуміло, хто його тримає.

Найпростіший спосіб — дописати f.close() у кінці. Він працює рівно доти, доки між open і close нічого не падає. А падає, як ми знаємо з теми 18, регулярно: виняток пропускає решту блоку, і рядок close() просто не виконується.

# наївно: close() не виконається, якщо всередині виняток f = open("zvit.txt", "w", encoding="utf-8") f.write(f"разом: {сума / кількість}\n") # кількість == 0 → ZeroDivisionError f.close() # сюди вже не дійдемо # чесно, але багатослівно f = open("zvit.txt", "w", encoding="utf-8") try: f.write(f"разом: {сума / кількість}\n") finally: f.close() # виконається завжди # те саме, коротше — і саме так пишуть with open("zvit.txt", "w", encoding="utf-8") as f: f.write(f"разом: {сума / кількість}\n")

Інтерактив 2 · Той самий код трьома способами

Перемкни сценарій на «виняток усередині» й порівняй три колонки: чи закрито файл і скільки рядків справді долетіло до диска.

close: наївний
close: finally
close: with
рядків на диску (A)
Сценарій «усе гаразд» — це і є причина, чому пастку не помічають. Усі три колонки однакові: файл закрито, два рядки на диску. Різниця зʼявляється рівно тоді, коли щось пішло не так, — тобто в найгіршу мить, коли ти й так розбираєшся з винятком, а тут ще й файл порожній.

Обʼєкт, який уміє стояти після with, називають менеджером контексту (context manager). Він знає дві дії: що зробити на вході в блок і що зробити на виході. Файловий обʼєкт на вході віддає сам себе (тому as f — це той самий обʼєкт, що повернула open), а на виході викликає close(). Вихід відбувається завжди: через кінець блоку, return, break чи виняток.

with закриває, але не рятує. Виняток, що стався всередині блоку, летить далі вгору рівно так само, як без with: менеджер контексту гарантує прибирання, а не тишу. Хочеш ще й обробити виняток — потрібен try/except навколо блоку.

Після виходу з блоку обʼєкт лишається, але вже закритий, і спроба скористатись ним дає впізнаваний виняток:

with open("prays.txt", encoding="utf-8") as f: вміст = f.read() print(f.closed) # True f.read() # ValueError: I/O operation on closed file.

Кілька файлів в одному with пишуть через кому — обидва закриються, навіть якщо впаде посередині:

with open("prays.txt", encoding="utf-8") as вхід, \ open("zvit.txt", "w", encoding="utf-8") as вихід: for рядок in вхід: вихід.write(рядок.upper())

05 / ПасткаКодування: чому utf-8 обовʼязково

Тепер головна практична пастка теми. На диску немає символів — там байти. Щоб з байтів вийшов текст, потрібна таблиця відповідності, і називають її кодуванням (encoding).

Таблиць історично багато. ASCII описує 128 символів: латиниця, цифри, розділові знаки — і жодної кириличної літери. cp1251 (вона ж Windows-1251) розширює ASCII до 256 і додає туди кирилицю — але лише її, і в кожної мовної групи була така своя таблиця. UTF-8 вміщає всі писемності світу: ASCII в ній лишився однобайтовим, а решта займає два-чотири байти.

Порахуймо руками на слові хліб. У cp1251 кожна літера — один байт: F5 EB B3 E1, разом чотири. У UTF-8 кирилична літера коштує два байти: D1 85 D0 BB D1 96 D0 B1, разом вісім. Той самий текст, різна довжина — і жодного натяку всередині файлу на те, за якою таблицею його писали. Кодування у файлі не зберігається. Його треба знати наперед.

Інтерактив 3 · Записали одним кодуванням, читаємо іншим

Той самий український рядок. Обери, чим писали й чим читаємо, — і подивись на нижній ряд.

байтів на диску
символів вийшло
збіглося з оригіналом
як дізнаєшся
Порівняй дві біди. «Пишемо utf-8, читаємо cp1251» — це 13 байтів, витлумачених як 13 однобайтових символів: хліб;28.5. Помилки немає, програма спокійно працює далі, і мотлох їде в базу даних. «Пишемо cp1251, читаємо utf-8» — це UnicodeDecodeError на першому ж байті. Гучна біда краща за тиху: про неї ти дізнаєшся в ту саму секунду.

Тепер найголовніше. Якщо не передати encoding, Python не вгадує кодування файлу — він бере налаштування системи. На сучасному Linux і macOS це UTF-8, а на Windows донедавна була кирилична cp1251. Ось звідки береться класична історія: у тебе все читається, у колеги той самий скрипт на тому самому файлі падає. Код однаковий, система різна.

Правило без винятків. Кожен open у текстовому режимі отримує encoding="utf-8". Завжди. Це чотирнадцять символів, які роблять поведінку програми однаковою на всіх машинах світу. Пропущений encoding — не «дрібна недбалість», а зламаність, яка проявиться не в тебе.

Що робити, коли файл таки прийшов у cp1251? Не вгадувати, а спитати того, хто його дав, і передати кодування явно: open("staryi.txt", encoding="cp1251"). Автовизначення — це евристика, і короткий файл вона вгадує особливо погано.

Якщо джерело брудне й кілька байтів псують усе читання, є аварійний вихід — аргумент errors:

open("brudno.txt", encoding="utf-8", errors="replace") # зіпсоване → «�» open("brudno.txt", encoding="utf-8", errors="ignore") # зіпсоване викидається

Обидва варіанти — втрата даних, і ставити їх «щоб не падало» не можна. Вони доречні там, де прочитати 99% тексту цінніше, ніж зупинитись: пошук по логах, груба статистика. У розборі платіжок такому місця немає.

Звідки береться utf-8-sig. Excel і низка редакторів Windows ставлять на початок файлу три службові байти EF BB BF — «мітку порядку байтів» (BOM). Прочитаний як звичайний utf-8, такий файл дасть невидимий символ \ufeff на початку, і перший заголовок стовпця раптом не збігається ні з чим. Кодування utf-8-sig зʼїдає цю мітку при читанні. Дивно поводиться перше поле CSV — підозра саме на BOM.

06 / ЧитанняТри способи прочитати

Файл відкрито. Далі є кілька способів дістати з нього текст, і вибір між ними — не питання смаку.

with open("prays.txt", encoding="utf-8") as f: for рядок in f: назва, ціна = рядок.rstrip("\n").split(";") print(назва, "→", float(ціна))

Обхід циклом тримає в памʼяті один рядок за раз: Python читає з диска блок у кілька кілобайтів, віддає з нього рядки, дочитує наступний — і так до кінця. Скільки б не важив файл, витрата памʼяті лишається сталою.

Інтерактив 4 · Скільки памʼяті просить кожен спосіб

Повзунок задає розмір файлу. Смуги показують памʼять у пікові, шкала — від найбільшого з трьох.

файл на диску
read()
readlines()
цикл
Зсунь повзунок у крайнє ліве положення. На файлі з десяти рядків найдорожчий спосіб — саме обхід циклом: вісім кілобайтів буфера проти кілобайта в readlines(). А тепер у крайнє праве: readlines() просить майже десять гігабайтів, а цикл — ті самі вісім кілобайтів. Перевага обходу не в тому, що він завжди економніший, а в тому, що він не росте. Модель приблизна: 40 байтів тексту на рядок і ще близько 62 службових на кожен обʼєкт-рядок у списку.
Символ \n лишається в рядку. І readline(), і обхід циклом віддають рядок разом із переходом на новий рядок, тому далі майже завжди йде .rstrip("\n"). Забудеш — і остання ціна буде "28.5\n". float її, до речі, проковтне без скарг (пробільні символи по краях він ігнорує), а от порівняння назва == "хліб" тихо стане хибним.

07 / ЗаписЗапис і дозапис

Метод write приймає рядок і повертає кількість записаних символів. Головне про нього — те, чого він не робить:

with open("zvit.txt", "w", encoding="utf-8") as f: f.write("хліб;28.5") f.write("молоко;32.0") # у файлі один рядок: хліб;28.5молоко;32.0

write не додає перехід на новий рядок. Ніколи. Це прямий наслідок того, що файл — послідовність символів, а не список рядків: якщо потрібен \n, пиши його сам.

writelines теж не додає \n. Назва обіцяє «записати рядки», а метод просто склеює елементи списку без жодного роздільника. Хочеш рядки — додай переходи сам: f.writelines(р + "\n" for р in назви). Часто читабельніший другий варіант — print("хліб;28.5", file=f): print додає \n сам, як і на екрані.

Режим "a" перетворює файл на журнал: кожен запуск програми дописує свій рядок у кінець, нічого не стираючи. Класичне застосування — протокол обробки:

with open("zhurnal.txt", "a", encoding="utf-8") as f: f.write(f"оброблено рядків: {len(рядки)}\n")

Межа тут одна: "a" дописує, але не прибирає. Файл журналу росте, доки хтось його не оберне, — і в маленькому скрипті достатньо про це памʼятати.

08 / Шляхиpathlib: шлях як обʼєкт

У темі 02 ми розібрали, що таке шлях і чим абсолютний відрізняється від відносного. Тоді шлях був для нас рядком — і саме так із ним довго й поводились у Python. Проблема в тому, що рядок нічого не знає про свою будову.

# так робити не варто тека = "dani" шлях = тека + "/" + "prays.txt" # на Windows роздільник \ шлях = тека + "prays.txt" # «daniprays.txt» — і жодної помилки

Клеїти шляхи рядками погано з двох причин. Перша: роздільник різний/ на Linux і macOS, \ у Windows. Друга, важливіша: рядок не перевіряє себе. Забув риску — програма спокійно створить файл daniprays.txt замість того, щоб поскаржитись.

Модуль pathlib розвʼязує це не функціями, а типом: шлях стає обʼєктом, який знає власну будову.

from pathlib import Path шлях = Path("dani") / "2026" / "prays.txt" print(шлях) # dani/2026/prays.txt (у Windows: dani\2026\prays.txt) print(type(шлях)) # <class 'pathlib.PosixPath'>

Оператор / тут — не хак і не гра слів: для Path ділення перевизначене й означає «додати складову шляху», а правильний роздільник система підставить сама. Клас теж обирається автоматично — PosixPath на Linux і macOS, WindowsPath у Windows, — і саме тому той самий код працює скрізь.

Інтерактив 5 · З чого складається Path

Обери властивість — підсвітиться те, що вона поверне. Перемикач стилю показує той самий шлях очима Windows.

що поверне
тип
складових у .parts
Найкорисніша кнопка тут — .suffixes. У назві prays.2026.csv дві крапки, і .suffix бере лише останню частину — .csv. «2026» для pathlib — не розширення, а шматок назви, тому .stem дає prays.2026. Хто цього не знає, той пише імʼя.split(".")[0] і одного дня втрачає половину назви файлу.

Найкорисніші властивості одразу списком, на прикладі Path("/home/olena/dani/prays.2026.csv"):

виразщо дає
.nameprays.2026.csv — імʼя з розширенням
.stemprays.2026 — імʼя без останнього розширення
.suffix.csv — останнє розширення, з крапкою
.parent/home/olena/dani — тека, у якій лежить
.partsкортеж усіх складових, від кореня
.with_suffix(".json")той самий шлях з іншим розширенням
Path.cwd()поточна тека — від неї рахуються відносні шляхи
Path(__file__).parentтека, у якій лежить сам скрипт
Path нічого не читає з диска. Path("немає-такого.txt") створюється без найменшої скарги — це просто обʼєкт із текстом усередині. Диска він торкається лише тоді, коли ти прямо просиш: .exists(), .read_text(), .glob(). Це зручно (шлях можна складати й передавати куди завгодно) і водночас підступно: сам факт, що обʼєкт створився, нічого не доводить про існування файлу.
Класична пастка «у мене працювало». Скрипт читає open("dani/prays.txt"): із теки проєкту все гаразд, з іншої теки — FileNotFoundError, бо відносний шлях рахується від поточної теки, а не від теки зі скриптом. Ліки одні: Path(__file__).parent / "dani" / "prays.txt". Змінна __file__ знайома нам із теми 16.

09 / ТекаТека: створити й обійти

Файли рідко бувають самі по собі — зазвичай їх багато й вони лежать у теках. pathlib закриває і цю роботу.

тека = Path("zvity") / "2026" тека.mkdir(parents=True, exist_ok=True) print(тека.exists(), тека.is_dir()) # True True

Обидва прапорці в mkdir потрібні, бо без них рядок падає через раз. parents=True створює всі проміжні теки — без нього zvity/2026 не зʼявиться, якщо немає zvity (це аналог mkdir -p з терміналу). exist_ok=True велить не скаржитись, якщо тека вже є: без нього повторний запуск програми впаде з FileExistsError, а повторний запуск — нормальний сценарій, не аварія.

Обійти вміст теки можна трьома способами. .iterdir() віддає все, що лежить безпосередньо всередині. .glob(шаблон) фільтрує за маскою. .rglob(шаблон) — те саме, що .glob("**/" + шаблон), тобто з заходом у всі вкладені теки.

Інтерактив 6 · Що саме знайде твій glob

Одне дерево файлів, шість шаблонів. Знайдене підсвічується зліва й перелічується справа.

знайдено
з них теки
найглибший рівень
усього в дереві12
Порівняй */*.csv і **/*.csv. Одна зірочка — рівно один рівень углиб, тому dani/stari/2024.csv лишається невидимим. Дві зірочки означають «ця тека і всі вкладені» — знаходяться всі три файли. І окремо подивись на *: серед пʼяти знахідок дві — теки, бо glob не відрізняє файл від теки, доки не спитаєш .is_file().

Дві властивості glob, про які легко забути. Він повертає генератор, а не список: диск читається поступово, у міру обходу, тому print(тека.glob("*.csv")) надрукує <generator object ...>, а не файли. І порядок не гарантований — він залежить від файлової системи, тому тест, який мовчки покладається на алфавіт, одного дня почервоніє на чужому компʼютері. Треба порядок — sorted(...).

for файл in sorted(Path("dani").glob("*.csv")): print(файл.name, файл.stat().st_size, "байтів")

Видалення теж на місці: файл.unlink(missing_ok=True) прибирає файл, тека.rmdir()порожню теку. Для теки з вмістом потрібен shutil.rmtree(), і це та функція, перед викликом якої варто двічі подивитись на змінну зі шляхом.

10 / СкороченняЯрлики read_text і write_text

Для найчастішого випадку — «прочитати невеликий файл цілком» — pathlib має готові методи, і вони самі відкривають та закривають файл:

шлях = Path("prays.txt") текст = шлях.read_text(encoding="utf-8") # замість with open(...) as f: f.read() шлях.write_text("хліб;28.5\n", encoding="utf-8") # повертає кількість записаних символів дані = шлях.read_bytes() # двійковий варіант

Аргумент encoding тут потрібен рівно так само, як в open: це скорочення запису, а не скасування правил. Не варто ними користуватись у двох випадках. Великий файлread_text тягне все в памʼять, точно як read(), і інтерактив 4 показав, чим це закінчується. Дозаписwrite_text завжди перезаписує файл з нуля, це "w", а не "a".

11 / ГлибшеДля тих, хто вже пише код

Кілька речей, які на робочому проєкті трапляються щотижня.

Буферизація й коли потрібен flush()

write не пише на диск. Він кладе дані в буфер у памʼяті, і лише коли буфер заповнився, вміст іде до операційної системи одним шматком. Системний виклик коштує на порядки дорожче за копіювання в памʼять — тому тисяча дрібних записів по 14 байтів мала б тисячу викликів замість двох.

Інтерактив 7 · Де насправді твої дані

Повзунок — скільки рядків уже записано. Буфер — 8 192 байти; дивись, скільки з них дійшло до диска.

усього байтів
на диску
у буфері
звернень до ОС
Найцікавіше — режим «просто пишемо» на малій кількості рядків. Записано 100 рядків = 1 400 байтів, а на диску нуль: усе в буфері, жодного звернення до ОС ще не було. «flush() щоразу» кладе все на диск негайно, але платить системним викликом за кожен рядок. Вихід із with робить те саме одним скиданням у кінці — і це та причина, чому with не просто охайніший, а правильніший.

Отже, f.flush() потрібен там, де файл читає хтось інший одночасно з записом: журнал під tail -f або файл прогресу довгої обробки. У решті випадків with скине буфер сам. І уточнення, яке рятує від хибної впевненості: flush() віддає дані операційній системі, а не кладе їх на пластину диска — в ОС теж є свій кеш. Гарантія переживання вимкнення живлення — це os.fsync(f.fileno()), і коштує вона відповідно.

newline="" для CSV

У текстовому режимі Python нормалізує кінці рядків: при читанні перетворює \r\n на \n, а при записі у Windows робить навпаки. Для звичайного тексту це благо. Для модуля csv — біда, бо він керує роздільниками рядків сам, і два шари нормалізації дають зайві порожні рядки через один. Тому файл під csv відкривають так:

with open("zvit.csv", "w", encoding="utf-8", newline="") as f: ...

Це не забобон: newline="" вимикає нормалізацію, лишаючи роботу з \r\n самому модулю csv. Знадобиться вже в темі 21.

Чому os.path досі трапляється

pathlib зʼявився в Python 3.4, але os.path старший на два десятиліття, і код із ним нікуди не подівся. Відповідники прямі: os.path.join(a, b) — це Path(a) / b, os.path.basename.name, os.path.splitext(x)[1].suffix. Читай як інший запис тієї самої думки; у новому коді пиши pathlib. Сумісність повна: майже будь-яка функція, що чекає шлях-рядок, приймає й Path — через протокол os.PathLike. Навпаки теж просто: str(шлях).

Ціна запитання до диска й атомарний запис

Path нічого не кешує: кожен .exists(), .is_file(), .stat() — окремий системний виклик. У циклі по сотні тисяч файлів це помітно, і тоді беруть os.scandir(): він віддає записи, у яких тип уже відомий з одного читання теки.

Друга річ, про яку згадують пізно: якщо програму вбити посеред write, на диску лишиться половина файлу — формально цілого, фактично зіпсованого. Захист один: писати в тимчасовий файл поруч, а потім перейменувати його на місце цільового. Перейменування в межах однієї файлової системи атомарне — або старий файл, або новий, проміжного стану не існує.

тимчасовий = шлях.with_suffix(".tmp") тимчасовий.write_text(текст, encoding="utf-8") тимчасовий.replace(шлях) # атомарна заміна

Знайти пропущений encoding автоматично

З Python 3.10 запуск python3 -X warn_default_encoding скрипт.py друкує EncodingWarning на кожен open без явного кодування. Прогнати цим прапорцем чужий проєкт — найшвидший спосіб знайти майбутні проблеми з кирилицею. Довгострокове рішення обговорюють у PEP 686: зробити UTF-8 кодуванням за замовчуванням, після чого пастка зникне сама. Доти — пиши encoding явно.

12 / ПідсумокЩо забрати з теми

Тепер програма вміє читати з диска й писати на диск. Але подивись, чим ми досі займались із прочитаним: рядок.rstrip("\n").split(";") — розбираємо текст руками. Це працює для трьох рядків, а щойно в назві товару трапиться сама крапка з комою або лапки, розбір розсиплеться. Домовленостей про те, як пакувати дані в текст, люди придумали чимало, і дві перемогли решту: CSV для таблиць і JSON для вкладених структур. Обидві мають готові модулі в стандартній бібліотеці, обидві правильно розбирають лапки й екранування — і про них тема 21. Файл ми відкривати вже вміємо; лишилось навчитись розуміти, що в ньому написано.

Далі в практиці. У practice.ipynb ти зробиш усе це руками в тимчасовій теці, яка сама прибереться після роботи: запишеш прайс і прочитаєш його трьома способами, побачиш на власні очі, як "w" знищує вміст, доведеш через assert, що with закрив файл навіть після винятку, відтвориш мотлох із інтерактиву 3 байт у байт, збудуєш дерево тек через mkdir(parents=True) і перевіриш усі шість шаблонів glob з інтерактиву 6 на справжніх файлах.

Далі в темі

Теорію прочитано. Тепер закріпи її на практиці.