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

Скрипти командного рядка

Одна програма, десятки різних запусків — і жодної правки коду. Аргументи, довідка, яку ніхто не писав, коди виходу й два потоки виводу.

Увесь цей блок ми вчилися працювати з реальністю: читати й писати файли, розбирати CSV і JSON, рахувати дати, шукати в тексті за зразком. Виходили корисні шматки коду — але жили вони в зошиті або у файлі, де шлях до даних був вписаний прямо в текст програми. Сьогодні ми перетворимо такий шматок на інструмент: програму, якій кажеш, що робити, прямо в момент запуску.

Це остання тема блоку, і вона склеює кілька попередніх у щось цілісне. Термінал ми відкривали ще у темі 02, функції писали в темі 14, загадковий рядок if __name__ == "__main__" розібрали в темі 16 й пообіцяли показати, навіщо він потрібен насправді. Обіцянку виконуємо тут.

01 / МотивПрограма, яку не треба правити

Уяви звичайну задачу. Є файл prodazhi.csv з колонками data, tovar, kilkist, suma. Треба порахувати підсумок по колонці suma. Перше, що напише кожен:

import csv

with open("prodazhi.csv", encoding="utf-8") as f:
    ryadky = list(csv.DictReader(f))

print(sum(float(r["suma"]) for r in ryadky))

Працює. Рівно один раз. Бо завтра прийде prodazhi-lypen.csv, післязавтра знадобиться сума не по suma, а по kilkist, а через тиждень колега попросить те саме для свого файлу. І щоразу доведеться відкривати код і правити рядок.

Здається, дрібниця. Насправді ця дрібниця коштує дорого, і ось чому:

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

python3 zvit.py prodazhi.csv
python3 zvit.py prodazhi-lypen.csv --kolonka kilkist
python3 zvit.py dani/kvartal.csv --dia serednye --tochnist 1

Слова після імені файлу називаються аргументами командного рядка (command-line arguments). Це рівно та сама ідея, що й параметри функції: тіло не змінюється, а поведінка залежить від того, що передали. Різниця лише в тому, хто передає. Функцію викликає інша функція, а скрипт викликає оболонка — і аргументи приходять не з коду, а з клавіатури.

Наскрізний приклад теми. Далі всюди буде один скрипт — zvit.py. Він читає CSV із продажами й друкує підсумок по числовій колонці. Ми доростимо його від трьох рядків до повноцінної утиліти з довідкою, перевіркою аргументів і чесними кодами виходу.

02 / Найпростішеsys.argv: список слів

Аргументи потрапляють у програму найпростішим можливим способом: Python кладе їх у список рядків sys.argv. Список, звичайні рядки, нічого більше.

import sys

print(sys.argv)

Запусти цей файл як python3 pokaz.py prodazhi.csv --tochnist 1 — і побачиш:

['pokaz.py', 'prodazhi.csv', '--tochnist', '1']

Три речі варто зауважити одразу, бо на кожній хтось спотикається.

  1. Нульовий елемент — це імʼя скрипта, а не інтерпретатора. Слова python3 у списку немає взагалі: воно адресоване оболонці, а не програмі. Тому «справжні» аргументи починаються з sys.argv[1].
  2. Усе всередині — рядки. '1', а не 1. Навіть якщо ти передав очевидне число, отримаєш текст, і sys.argv[3] + 1 дасть TypeError. Перетворення типу — твоя робота.
  3. Рядок на слова ріже не Python. Це робить оболонка ще до запуску програми. Python отримує вже готовий список.

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

Інтерактив 1 · З рядка в список

Що саме бачить програма. Сірим — те, що дістається оболонці й до Python не доходить.

довжина argv
аргументів
Головне: у теці лежать prodazhi.csv і prodazhi-lypen.csv. Зверни увагу на варіант із зірочкою: програма ніколи не бачить символ * — до неї доходять уже розкриті імена. І на варіант без лапок: пробіл усередині назви файлу мовчки розрізає її на два аргументи, а програма впаде на нібито неіснуючому файлі.
Дрібниця, яку варто знати. sys.argv[0] не завжди імʼя файлу. При запуску python3 -c "код" там опиниться рядок '-c', при python3 -m модуль — повний шлях до модуля, а в інтерактивному REPL — порожній рядок. Тому будувати логіку на argv[0] — погана ідея; для повідомлень краще брати sys.argv[0] лише як підказку, а справжнє імʼя програми задавати явно.

03 / МежаДе sys.argv перестає тішити

Поки аргумент один, sys.argv — ідеальний інструмент. Проблеми починаються, щойно зʼявляється другий. Порахуймо, що доведеться написати руками:

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

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

Ліворуч — руками через sys.argv, праворуч — через argparse. Вимоги додаються зліва направо.

руками · sys.argv
через argparse
рядків руками
рядків argparse
Що читати: дивись не на різницю в довжині, а на характер лівого коду. Це не логіка задачі — це нескінченні if, try і зсуви лічильника. Кожен із них можна написати неправильно, і саме так у скриптах зʼявляються помилки, які помічають через півроку.

І це ще оптимістична картина. Ліва колонка не вміє того, що права дає безкоштовно: не розуміє злиті прапорці, не приймає форму --tochnist=1, не підказує схожу назву при одруку, не вміє зупинити розбір на --. Дописати можна все — питання лише в тому, навіщо, якщо це вже написано й лежить у стандартній бібліотеці.

04 / Інструментargparse за пʼять рядків

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

крок 1 · створити розбирач
parser = argparse.ArgumentParser(description="Підсумок по колонці CSV.")

Обʼєкт, який зберігатиме опис усіх аргументів. Текст із description зʼявиться в довідці — це єдине, для чого він потрібен.

крок 2 · описати аргументи
parser.add_argument("fajl", help="шлях до CSV-файлу")

По одному виклику на кожен аргумент. Перший рядок — імʼя, решта — уточнення: тип, типове значення, перелік дозволених варіантів, текст довідки.

крок 3 · розібрати
args = parser.parse_args()

Тут відбувається все: argparse сам бере sys.argv, звіряє його з описом, перетворює типи, підставляє типові значення — а якщо щось не так, друкує зрозуміле повідомлення й завершує програму. До наступного рядка виконання просто не дійде.

Результат parse_args() — обʼєкт Namespace, у якого значення лежать атрибутами: args.fajl, args.tochnist. Це не словник, звертатися через квадратні дужки не вийде. Надрукувати його цілком корисно під час налагодження:

Namespace(fajl='prodazhi.csv', kolonka='suma', dia='serednye', tochnist=1, tyho=True)

І найголовніше: разом із цими трьома кроками ти безкоштовно отримав довідку. Ніхто її не писав — argparse зібрав її з опису аргументів. Спробуй запустити будь-який скрипт із -h, і побачиш готовий текст, який завжди відповідає дійсності, бо генерується з того самого джерела, що й розбір.

05 / ВидиПозиційні, необовʼязкові, прапорці

Аргументи бувають трьох сортів, і argparse розрізняє їх за однією ознакою — чи починається імʼя з дефіса.

сортяк описатияк передатиповедінка
позиційнийadd_argument("fajl")zvit.py dani.csvобовʼязковий, розпізнається за місцем
необовʼязковийadd_argument("-k", "--kolonka")--kolonka sumaможна пропустити, порядок вільний
прапорецьadd_argument("--tyho", action="store_true")--tyhoзначення не має: є або немає

Позиційний аргумент — той, чиє призначення визначається місцем у рядку. Він обовʼязковий за замовчуванням: якщо не передати, argparse лайне й вийде. Імʼя, яке ти дав, стає і назвою атрибута, і підписом у довідці.

Необовʼязковий — той, що починається з дефіса. Прийнято описувати дві форми одразу: коротку -k для щоденного набору й довгу --kolonka для читабельності в скриптах. Імʼя атрибута argparse бере з довгої форми, відкидає дефіси й міняє решту дефісів на підкреслення: --vyvid-fajl стане args.vyvid_fajl.

Прапорець — окремий випадок необовʼязкового. action="store_true" означає «якщо аргумент є в рядку, поклади True, інакше False». Значення після нього не пишуть. Саме тому прапорці називають так: вони або підняті, або ні.

Типова пастка новачка. action="store_true" легко переплутати з type=bool. Другий варіант виглядає логічно, але працює не так, як здається: bool("False") — це True, бо непорожній рядок істинний. Тобто --tyho False увімкне режим замість вимкнути. Для «так або ні» правильна відповідь завжди action="store_true".

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

Інтерактив 3 · Конструктор argparse

Обери, які аргументи має приймати zvit.py. Довідку ніхто не пише — вона збирається з опису.

zvit.py — код розбирача
python3 zvit.py -h — цей текст згенеровано
add_argument
рядків довідки
Зверни увагу на два рядки, яких ти не додавав: -h, --help у переліку й слово usage: зверху. argparse дописує довідку сам і сам же вміє її показати. Це не косметика: саме тому скрипт, зроблений через argparse, залишається зрозумілим через рік — достатньо запустити його з -h.

06 / Уточненняtype, default, choices, nargs

Один виклик add_argument приймає кілька уточнень, і кожне знімає з тебе окрему ручну перевірку.

уточненнящо робитьщо знімає з тебе
type=intперетворює рядок обраною функцієюtry/except ValueError
default=2значення, коли аргумент не передалиперевірку «а чи вказали?»
choices=[…]дозволяє лише перелічені значенняперевірку на список допустимих
required=Trueробить необовʼязковий обовʼязковимперевірку на None
nargs="+"приймає кілька значень у списокручний збір решти argv
metavar="N"як значення зветься в довідці

type= — найкорисніше. Важлива тонкість: туди передають функцію, а не назву типу. int і float — це функції, тому вони й працюють. Але туди можна віддати будь-яку функцію, яка приймає рядок і повертає значення: type=Path одразу дасть обʼєкт з pathlib, а власна функція може перевіряти, що число додатне, і кидати argparse.ArgumentTypeError із людським поясненням.

default= без явного значення дорівнює None. Дрібна зручність: у тексті help можна написати %(default)s, і argparse підставить туди фактичне типове значення. Тоді довідка не бреше навіть після того, як ти передумав.

nargs= задає, скільки значень поглинає аргумент: "+" — одне або більше (список ніколи не порожній), "*" — нуль або більше, "?" — нуль або одне, ціле число — рівно стільки. Перші два дають список, і це варто памʼятати: nargs="+" перетворює args.fajl на args.fajly, по якому треба ходити циклом.

Про required=True. Технічно можна зробити --kolonka обовʼязковим. Але подумай, що це означає: користувач мусить писати --kolonka завжди, хоча «необовʼязковий» у назві натякає на протилежне. Спільнота домовилась просто: обовʼязкове — позиційне, необовʼязкове — з дефісами. Порушувати цю угоду варто лише тоді, коли обовʼязкових речей кілька й запамʼятати їхній порядок неможливо.

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

Інтерактив 4 · Куди йде кожне слово

Ліворуч — слова з командного рядка, праворуч — поля Namespace. Сірим — значення за замовчуванням.

код виходу
полів заповнено
Опис розбирача той самий, що в конструкторі вище: позиційний fajl, -k/--kolonka з типовим suma, -d/--dia з переліком дозволених, -t/--tochnist цілим числом і прапорець --tyho. Зверни увагу на третій сценарій: необовʼязкові аргументи можуть стояти перед позиційним — порядок їм байдужий.

07 / ВідповідьКоди виходу

Кожна програма, завершуючись, повертає операційній системі одне ціле число. Воно зветься кодом виходу (exit code, або status code) і відповідає рівно на одне питання: все пройшло добре чи ні.

Домовленість стара, проста й спільна для всіх мов:

кодщо означаєзвідки береться в Python
0успіхпрограма дійшла до кінця без винятку
1помилка виконанняsys.exit(1) або неперехоплений виняток
2помилка в аргументахargparse при поганому командному рядку
3…125свої значеннящо завгодно, що ти задокументував

Побачити код останньої команди можна одразу в терміналі: echo $? на macOS і Linux, echo $LASTEXITCODE у PowerShell. Нуль — усе гаразд; будь-яке інше число — щось пішло не так.

Чому це має значення для скрипта, який пишеш ти? Бо твій скрипт майже напевно запускатиме не людина. Розклад завдань, сервер збірки, інший скрипт — усі вони не читають твої повідомлення. Вони дивляться на число. І якщо програма впала, але повернула нуль, ланцюг спокійно поїде далі з порожнім або зіпсованим файлом.

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

Інтерактив 5 · Ланцюг команд через &&

Три команди підряд. Ненульовий код зупиняє ланцюг, нульовий пропускає далі.

підсумковий echo $?
Головний висновок — у перемикачі праворуч. Крапка з комою просто розділяє команди: вони виконаються всі, а підсумковий код буде від останньої. Тобто збій посередині ланцюга зникає безслідно. Саме так нічні завдання роками «успішно» відправляють порожні звіти.
Пастка sys.exit. Функція приймає не лише число. sys.exit("файл не знайдено") надрукує цей рядок у stderr і поверне код 1. Зручно, але легко переплутати з print: рядок піде не туди, куди ти думав. І окремо: sys.exit(0) усередині try/except буде перехоплений блоком except Exception? Ні — SystemExit успадковується не від Exception, а від BaseException саме для цього. А от голий except: його проковтне, і програма не завершиться (детальніше про ієрархію — у темі 18).

08 / Потокиstdout проти stderr

У кожної програми, яка працює в терміналі, є три канали звʼязку зі світом. Їх називають стандартними потоками:

Звичайний print пише в stdout. Щоб написати в stderr, достатньо додати один іменований аргумент:

print(pidsumok)  # результат → stdout
print("оброблено 1240 рядків", file=sys.stderr)  # звіт про перебіг → stderr

Навіщо два потоки, якщо обидва зрештою потрапляють на той самий екран? Бо на екран вони потрапляють лише тоді, коли програму запустила людина. Оболонка вміє перенаправляти їх окремо, і саме тут різниця стає відчутною:

записщо робить
> zvit.txtstdout — у файл; stderr лишається на екрані
2> pomylky.logstderr — у файл; stdout лишається на екрані
| sortstdout — на вхід наступної програми; stderr на екран
> zvit.txt 2>&1обидва в один файл (двійка «стає» одиницею)
2> /dev/nullstderr викинути зовсім

Звідси правило, яке відрізняє акуратний скрипт від неохайного: у stdout іде тільки результат. Усе інше — у stderr. Заголовки, індикатори прогресу, «читаю файл…», попередження — усе це в другий потік. Перемкни, куди пише програма, і подивись, що станеться:

Інтерактив 6 · Куди що потрапляє

Той самий скрипт друкує два рядки: результат і службове повідомлення.

у файлі / далі по трубі
Увімкни перемикач і обери «| sort» — побачиш точну причину, чому в правило вірять. Службовий рядок, надрукований у stdout, потрапляє в трубу разом із даними, і наступна програма намагається його обробити як дані. Помилка не падає — вона тихо псує результат.
Для профі: буферизація. Коли stdout веде не на термінал, а у файл чи в трубу, Python буферизує його блоками по кілька кілобайтів, а stderr з версії 3.9 лишається порядковим. Побічний ефект помітний одразу: у файлі > usе.log 2>&1 рядки з двох потоків можуть перемішатися не в тому порядку, у якому їх друкували. Ліки — print(..., flush=True) у важливих місцях або запуск з python3 -u.

09 / ТрубаКонвеєр і stdin

Вертикальна риска | зветься конвеєром (pipe) і робить рівно одну річ: зʼєднує stdout лівої програми зі stdin правої. Це той механізм, завдяки якому дрібні утиліти складаються в довгі ланцюги:

cat prodazhi.csv | grep "2026-07" | python3 zvit.py -

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

import sys

for ryadok in sys.stdin:
    print(len(ryadok.rstrip("\n")))

Домовленість, яку розуміють майже всі утиліти: одинокий дефіс замість імені файлу означає «читай зі stdin». У argparse це робиться однією конструкцією — type=argparse.FileType("r") сам відкриє файл, а на дефісі підставить sys.stdin.

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

І ще одна деталь, на якій спотикаються всі, хто вперше пише утиліту для конвеєра. Команда python3 zvit.py velykyj.csv | head -3 завершиться помилкою BrokenPipeError: head прочитала три рядки, закрилася, а твоя програма все ще пише в трубу, якої вже немає. Це не поломка — це нормальний кінець життя в конвеєрі, і акуратний скрипт ловить цей виняток та тихо виходить.

10 / ФормаСтруктура зрілого скрипта

Тепер зберімо все докупи. Ось zvit.py цілком — у тому вигляді, у якому такі речі пишуть у справжніх проєктах:

#!/usr/bin/env python3
"""Підсумок по числовій колонці CSV-файлу з продажами."""
import argparse
import csv
import sys

def zrobyty_rozbyrach():
    # окрема функція, щоб розбирач можна було перевірити в тесті
    parser = argparse.ArgumentParser(
        description="Рахує підсумок по колонці CSV-файлу з продажами.")
    parser.add_argument("fajl", help="шлях до CSV-файлу")
    parser.add_argument("-k", "--kolonka", default="suma",
                    metavar="НАЗВА",
                    help="назва колонки (типово: %(default)s)")
    parser.add_argument("--tyho", action="store_true",
                    help="друкувати лише число, без заголовка")
    return parser

def main(argv=None):
    args = zrobyty_rozbyrach().parse_args(argv)
    try:
        with open(args.fajl, encoding="utf-8", newline="") as f:
            ryadky = list(csv.DictReader(f))
    except OSError as pomylka:
        # повідомлення — у stderr, щоб не змішатися з результатом
        print(f"zvit.py: {pomylka}", file=sys.stderr)
        return 1

    if not ryadky or args.kolonka not in ryadky[0]:
        print(f"zvit.py: немає колонки {args.kolonka!r}",
              file=sys.stderr)
        return 1

    pidsumok = sum(float(r[args.kolonka]) for r in ryadky)
    # результат — і тільки він — іде в stdout
    if args.tyho:
        print(f"{pidsumok:.2f}")
    else:
        print(f"сума по колонці {args.kolonka}: {pidsumok:.2f}")
    return 0

if __name__ == "__main__":
    sys.exit(main())

У цій формі три рішення, і кожне має причину.

1 · уся робота в функції main

Не на верхньому рівні файлу. Тоді модуль можна імпортувати, і нічого не станеться — рівно те, про що говорила тема 16: у файлі, який хтось може імпортувати, на верхньому рівні тільки означення.

2 · main повертає код, а не викликає sys.exit

Це різниця між «функцію можна протестувати» і «функція вбиває тест». Виклик main(["prodazhi.csv", "--tyho"]) з тесту поверне число, яке легко порівняти. Якби всередині стояв sys.exit, тест довелося б обгортати в pytest.raises(SystemExit) і діставати код із винятку.

3 · sys.exit(main()) під захисною умовою

Ось де код виходу нарешті потрапляє в операційну систему. main() повертає число, sys.exit віддає його назовні. Один рядок — і скрипт чесно відповідає ланцюгу з &&.

Параметр argv=None у main — теж не випадковість. parse_args(None) означає «бери sys.argv[1:]», тобто при звичайному запуску все працює як зазвичай. А в тесті можна передати свій список і перевірити скрипт, не запускаючи окремий процес.

Перший рядок із #!. Він зветься shebang і потрібен на macOS та Linux: після chmod +x zvit.py файл можна запускати як ./zvit.py prodazhi.csv, без слова python3. Форма #!/usr/bin/env python3 краща за прямий шлях, бо шукає інтерпретатор у PATH — а отже підхопить той, що з віртуального середовища. На Windows рядок ігнорується як звичайний коментар.

11 / ПрактикаТренажер: запусти сам

Теорія закінчилась. Нижче — модель того самого zvit.py: не справжній Python, але розбір аргументів, повідомлення про помилки й коди виходу відтворені точно. У теці лежить prodazhi.csv — 1240 рядків із колонками data, tovar, kilkist, suma.

Інтерактив 7 · Тренажер запуску

Пиши команду й тисни Enter. Червоним — те, що йде в stderr.

olena@mac ~ %
код виходу
рядків stdout
Три завдання праворуч. Найшвидший спосіб пройти їх — почати з python3 zvit.py -h: довідка сама розкаже, які аргументи скрипт приймає. Спробуй також echo $? після невдалого запуску й перемикач «сховати stderr»: результат лишиться на місці, а повідомлення зникнуть.

12 / ГлибшеДля тих, хто вже писав скрипти

Далі — речі, які новачку поки не потрібні, але економлять час тому, хто вже щось писав.

argparse проти click і typer

argparse — у стандартній бібліотеці, нічого ставити не треба, але описувати аргументи доводиться багатослівно. click робить те саме декораторами над функцією: менше тексту, зручні підкоманди, кольори й інтерактивні запити з коробки. typer іде далі й будує інтерфейс просто з сигнатури функції та її анотацій типів — імена параметрів стають іменами аргументів, а типи — перетворювачами.

Обидва — сторонні пакети, тобто плюс залежність. Просте правило: якщо скрипт має жити поруч із даними й запускатися будь-де без установки — argparse. Якщо це утиліта проєкту з десятком підкоманд — typer зекономить помітно більше, ніж коштує залежність.

Підкоманди

Коли утиліта вміє кілька різних дій, аргументами це виражати незручно. Правильний спосіб — підкоманди, як у git commit чи pip install. У argparse їх дає parser.add_subparsers(): кожна підкоманда отримує власний розбирач із власними аргументами й власною довідкою. Правило вибору просте: різні дії з різними наборами параметрів — підкоманди; одна дія з варіаціями — choices.

Чому не можна друкувати в stdout зайве

Ми вже бачили це в інтерактиві, але варто сформулювати як закон. Усе, що потрапило в stdout, є частиною результату — не тому, що так гарніше, а тому, що наступна програма в конвеєрі не має способу відрізнити твій дружній заголовок від даних. Рядок «Обробляю файл…», надрукований у stdout, у ланцюгу | sort | head -1 перетвориться на першу «найменшу» стрічку звіту.

Практичний наслідок: у скрипті, який щось віддає, print без file=sys.stderr має бути рівно там, де ти віддаєш результат. Усе решта — прогрес, попередження, «знайдено 12 файлів» — у stderr. Тоді і людина все побачить, і конвеєр не зламається.

Дрібниці, які тішать

13 / ПідсумокЩо ми тепер уміємо

Ми навчилися перетворювати код на інструмент, яким може скористатися хтось інший — або ти сам через півроку:

На цьому блок 5 закінчено. Озирнись: ми навчилися читати й писати файли, розуміти табличні й вкладені формати, працювати з датами й текстом — а сьогодні ще й запускати все це ззовні. Цього достатньо, щоб написати справжню робочу утиліту.

І саме тут стає видно наступну межу. Подивись на наш zvit.py: усередині ходять голі рядки й числа. Рядок з args.kolonka, список словників із CSV, число-підсумок. Поки програма робить одну дію, це нормально. Але щойно зʼявиться поняття «продаж» зі своєю датою, товаром, кількістю й сумою — і власними правилами, як його перевіряти, порівнювати й друкувати, — таскати це в словниках стане боляче. Потрібен спосіб описати власний тип: сказати, які дані має продаж і що з ним можна робити. Це класи й обʼєкти, з яких починається блок 6.

Далі в практиці. У practice.ipynb ти напишеш справжній скрипт із argparse у тимчасовій теці, запустиш його через subprocess з різними аргументами й перевіриш assert-ами найважливіше: що код виходу нульовий при успіху й другий при поганих аргументах, що повідомлення про помилку пішло в stderr, а не в stdout, і що довідка -h справді згенерувалася сама.

Далі в темі

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