Українська версія

Питання Python для QA Automation співбесіди

Практичні питання та відповіді з Python українською для QA Automation: core language, структури даних, OOP, pytest, web/API automation, code examples і практичні задачі.
160питань
Core language and syntaxJuniorVery commonTheory

Python — інтерпретована чи компільована мова, і чому це має практичне значення?

Відповідь

CPython спочатку компілює вихідний код у платформонезалежний байткод (кешується у файлах .pyc), а потім інтерпретатор CPython виконує цей байткод інструкція за інструкцією — а не «рядок за рядком» у сенсі вихідного коду, бо один рядок джерела може розгорнутися в багато інструкцій байткоду. Це не чисто інтерпретована і не компільована у машинний код мова, як C. На практиці це означає, що синтаксичні помилки Python виявляє рано, а більшість помилок імен і типів проявляються лише під час виконання, бо ніхто їх заздалегідь не перевіряє.

Сильна відповідь включає

  • код у байткод у інтерпретатор
  • кеш байткоду .pyc
  • інтерпретатор виконує інструкції байткоду, а не рядки джерела

Практичний приклад

Команда `python app.py` спочатку компілює app.py в байткод у __pycache__/app.cpython-314.pyc, а потім інтерпретатор виконує цей байткод; помилка в рідко викликаній функції проявиться лише тоді, коли цей код виконається.

#fundamentals#runtime-model

Джерела

The Python Language ReferencePython Software Foundation · Precise scoping, evaluation order and expression semanticsdis — Disassembler for Python bytecodePython Software Foundation · Bytecode execution model used to reason about interpreter performance
Core language and syntaxJuniorVery commonTheory

Що означає динамічна типізація в Python і як з нею пов'язана duck typing (качина типізація)?

Відповідь

Динамічна типізація означає, що змінна — це просто ім'я, прив'язане до об'єкта, а тип належить об'єкту, а не імені, тож те саме ім'я можна повторно прив'язати до значення іншого типу під час виконання. Duck typing випливає з цього: код, який викликає .read() на аргументі, працює з будь-яким об'єктом, що реалізує .read(), незалежно від його класу, бо Python перевіряє можливості через пошук атрибутів і методів, а не оголошений тип.

Сильна відповідь включає

  • тип належить об'єкту, а не імені
  • повторна прив'язка імені до іншого типу
  • можливості важливіші за оголошений тип

Практичний приклад

Функція, що викликає `item.upper()`, працює зі str, а також з будь-яким власним об'єктом, що має метод `upper()` — спільний базовий клас не потрібен.

#typing#duck-typing

Джерела

The Python Language ReferencePython Software Foundation · Precise scoping, evaluation order and expression semanticsPEP 20 — The Zen of PythonPython Software Foundation · Design philosophy behind idiomatic Python choices
Core language and syntaxJuniorVery commonTheory

Які вбудовані типи є змінюваними (mutable), а які незмінюваними (immutable), і чому це важливо для значень аргументів за замовчуванням і хешування?

Відповідь

list, dict, set і bytearray — змінювані; int, float, str, tuple, frozenset і bytes — незмінювані. Більшість незмінюваних вбудованих типів безпечно хешувати й використовувати як ключі словника чи елементи множини, бо їхнє значення не може змінитися після створення, але незмінюваний контейнер на кшталт tuple хешований, лише якщо кожен його елемент сам хешований — кортеж, що містить список, наприклад, все ще нехешований. Незмінюваність також пояснює класичну помилку зі змінюваним значенням за замовчуванням: `def f(items=[])` створює один об'єкт списку, який повторно використовується й змінюється при кожному виклику без власного аргументу, бо значення за замовчуванням обчислюється один раз при визначенні функції.

Сильна відповідь включає

  • змінювані проти незмінюваних вбудованих типів
  • незмінювані контейнери хешовані лише якщо хешований кожен елемент
  • пастка зі змінюваним аргументом за замовчуванням

Практичні приклади

Змінюване значення за замовчуванням: помилка і виправлення
def add_item(item, bucket=[]):
    bucket.append(item)
    return bucket

print(add_item("a"))
print(add_item("b"))

# Safe version
def add_item_safe(item, bucket=None):
    if bucket is None:
        bucket = []
    bucket.append(item)
    return bucket

print(add_item_safe("a"))
print(add_item_safe("b"))

Перша функція повторно використовує один список, створений під час визначення функції, тому стан протікає між викликами. Безпечна версія створює новий список для кожного виклику без bucket.

Очікуваний результат: Помилкові виклики друкують ['a'], потім ['a', 'b']; безпечні — ['a'], потім ['b'].

#mutability#gotchas

Джерела

The Python Standard LibraryPython Software Foundation · Authoritative behavior for built-in types and standard library modulesData modelPython Software Foundation · Dunder methods, object identity, attribute lookup and the descriptor and context manager protocols
Core language and syntaxJuniorVery commonTheory

Яка різниця між `is` та `==`, і коли варто використовувати кожен із них?

Відповідь

`is` порівнює ідентичність об'єктів — чи вказують два імені на той самий об'єкт у пам'яті, тоді як `==` порівнює рівність значень через виклик `__eq__`, який клас може перевизначити. Використовуйте `is` для порівняння з синглтонами, наприклад `x is None`, `x is True` або сентинел-об'єктами, а `==` — для порівняння реальних даних, що зберігають дві змінні. Покладатися на `is` для порівняння значень чисел чи рядків ненадійно, бо це залежить від деталей реалізації інтернування.

Сильна відповідь включає

  • ідентичність проти рівності значень
  • is для перевірки None/синглтонів
  • інтернування робить is ненадійним для значень

Практичні приклади

Ідентичність проти рівності
a = [1, 2]
b = [1, 2]
c = a

print(a == b)
print(a is b)
print(a is c)

value = None
print(value is None)

== перевіряє рівність значень; is перевіряє, чи два імені посилаються на той самий об'єкт. Перевірки синглтонів на кшталт None слід робити через is.

Очікуваний результат: True, False, True, True.

#identity#equality#gotchas

Джерела

Data modelPython Software Foundation · Dunder methods, object identity, attribute lookup and the descriptor and context manager protocolsThe Python Language ReferencePython Software Foundation · Precise scoping, evaluation order and expression semantics
Core language and syntaxMiddleCommonTheory

Поясніть правило LEGB, за яким Python визначає область видимості імені.

Відповідь

Python шукає просте ім'я у чотирьох областях видимості по черзі: Local (поточна функція), Enclosing (охоплююча функція, для замикань), Global (верхній рівень модуля) і Built-in (вбудовані імена Python). Пошук зупиняється на першій області, де ім'я знайдено, тож локальна змінна перекриває глобальну з тим самим ім'ям, а ім'я, якому присвоюють значення будь-де всередині функції, вважається локальним для всієї функції, якщо не оголошене через `global` чи `nonlocal`.

Сильна відповідь включає

  • порядок Local, Enclosing, Global, Built-in
  • перемагає перший знайдений збіг
  • присвоєння будь-де робить ім'я локальним

Практичний приклад

Якщо функція виконує `count = count + 1` без оголошення `global count`, Python видасть UnboundLocalError, бо присвоєння робить `count` локальним для всього тіла функції, включно з читанням до цього присвоєння.

#scoping#closures

Джерела

The Python Language ReferencePython Software Foundation · Precise scoping, evaluation order and expression semanticsThe Python TutorialPython Software Foundation · Baseline syntax, control flow, functions and module basics
Core language and syntaxMiddleCommonTheory

Яка різниця між ключовими словами `global` і `nonlocal`?

Відповідь

`global` вказує Python, що присвоєння всередині функції має змінювати ім'я в глобальній області модуля, а не створювати нову локальну змінну. `nonlocal` робить аналогічне для області видимості охоплюючої функції у вкладеному замиканні, пропускаючи локальну область, але зупиняючись перед глобальною. `nonlocal` викликає SyntaxError, якщо відповідної прив'язки немає в жодній охоплюючій функції, тоді як `global` спокійно створить ім'я на рівні модуля, якщо його ще не існує.

Сильна відповідь включає

  • global націлений на область модуля
  • nonlocal націлений на область охоплюючої функції
  • nonlocal вимагає наявної прив'язки в охоплюючій функції

Практичний приклад

Замикання `make_counter()` використовує `nonlocal count` у внутрішній функції `increment()`, щоб повторні виклики повернутої `increment` змінювали ту саму змінну `count`, захоплену з `make_counter`.

#scoping#closures

Джерела

The Python Language ReferencePython Software Foundation · Precise scoping, evaluation order and expression semantics
Core language and syntaxJuniorVery commonTheory

Як Python визначає, чи є довільний об'єкт truthy (правдивим) чи falsy (хибним) у булевому контексті?

Відповідь

Python викликає `__bool__` на об'єкті, якщо він визначений; якщо ні — переходить до `__len__` і вважає нульову довжину хибною; якщо жоден не визначений, об'єкт за замовчуванням правдивий. Вбудовані хибні значення: `None`, `False`, `0`, `0.0`, порожні рядки та порожні контейнери на кшталт `[]`, `{}`, `()` і `set()`. Саме тому `if my_list:` — ідіоматичний спосіб перевірити на порожнечу замість `if len(my_list) > 0:` чи `if my_list == []:`.

Сильна відповідь включає

  • спершу __bool__, потім __len__
  • вбудовані хибні значення
  • ідіоматичні перевірки на порожнечу

Практичний приклад

Власний клас `Queue`, що визначає `__len__`, буде хибним, коли порожній, тож `if not queue:` коректно виявляє порожню чергу без окремого методу `is_empty()`.

#truthiness#dunder-methods

Джерела

Data modelPython Software Foundation · Dunder methods, object identity, attribute lookup and the descriptor and context manager protocolsThe Python Language ReferencePython Software Foundation · Precise scoping, evaluation order and expression semantics
Core language and syntaxMiddleCommonTheory

Що робить оператор моржа (`:=`) і де він допомагає уникнути повторного обчислення?

Відповідь

Оператор моржа, введений PEP 572 у Python 3.8, присвоює значення імені як частину більшого виразу замість окремого оператора, і сам вираз обчислюється до цього ж значення. Найкорисніший він в умовах циклу `while`, у comprehension та умовах `if`, де те саме значення інакше довелося б обчислювати двічі: раз для перевірки й раз для використання.

Сильна відповідь включає

  • присвоєння як частина виразу
  • введений PEP 572 у 3.8
  • уникає подвійного обчислення значення

Практичний приклад

`while (chunk := file.read(8192)):` читає і присвоює за один крок, замінюючи старіший підхід із читанням перед циклом і повторним читанням у кінці тіла циклу.

#syntax#pep-572

Джерела

PEP 572 — Assignment ExpressionsPython Software Foundation · The walrus operator and its scoping rulesWhat's new in Python 3.14Python Software Foundation · Current-version language and runtime changes, including template strings and free-threading support
Core language and syntaxMiddleCommonTheory

Чим структурне зіставлення `match`/`case` в Python відрізняється від switch-оператора в стилі C?

Відповідь

Введений у Python 3.10 через PEP 634, `match`/`case` зіставляє форму й структуру значення, а не лише пряму рівність константі. Патерни можуть деструктурувати послідовності, словники й екземпляри класів, прив'язувати знайдені підзначення до імен, додавати умови-охоронці `if` і комбінувати варіанти через `|`. `case _:` виступає символом-заповнювачем за замовчуванням, і на відміну від switch у C тут немає неявного проходження (fall-through) між гілками.

Сильна відповідь включає

  • структурне зіставлення, а не лише рівність
  • введено PEP 634 у 3.10
  • немає fall-through, заповнювач — case _

Практичний приклад

`match command.split():\n case ["go", direction]: move(direction)\n case ["look"]: describe_room()\n case _: print("unknown command")` деструктурує розбитий на слова рядок команди прямо в патерні.

#pattern-matching#pep-634

Джерела

PEP 634 — Structural Pattern Matching: SpecificationPython Software Foundation · match/case semantics introduced in Python 3.10What's new in Python 3.14Python Software Foundation · Current-version language and runtime changes, including template strings and free-threading support
Core language and syntaxJuniorCommonPractical

Як Python обмінює значення двох змінних без тимчасової змінної, і як працює розширена розпаковка зі `*`?

Відповідь

`a, b = b, a` працює, бо оператор присвоєння Python спочатку повністю обчислює правий список виразів — отримуючи впорядковану послідовність старих значень `b` і `a`, — і лише потім присвоює ці значення лівим цілям по порядку, тож окрема тимчасова змінна в коді користувача не потрібна. (Чи справді CPython виділяє об'єкт кортежу для цього, чи оптимізує це в прямий обмін на стеці — деталь реалізації; мова гарантує лише порядок «спершу обчислення, потім присвоєння», а не конкретне виділення пам'яті.) Розширена розпаковка дозволяє імені зі зірочкою поглинути будь-яку кількість елементів, що залишилися: `first, *middle, last = values` прив'язує `first` і `last` до країв, а решту збирає в список `middle`.

Сильна відповідь включає

  • права частина повністю обчислюється перед будь-яким присвоєнням
  • явна тимчасова змінна в коді користувача не потрібна
  • ім'я зі зірочкою збирає решту елементів у список

Практичний приклад

`first, *middle, last = [1, 2, 3, 4, 5]` встановлює `first = 1`, `last = 5` та `middle = [2, 3, 4]`.

#unpacking#syntax

Джерела

The Python TutorialPython Software Foundation · Baseline syntax, control flow, functions and module basicsThe Python Language ReferencePython Software Foundation · Precise scoping, evaluation order and expression semantics
Core language and syntaxSeniorOccasionalTroubleshooting

Чому `a is b` іноді повертає True для двох окремо написаних літералів int чи str, і чому покладатися на це небезпечно?

Відповідь

CPython кешує малі цілі числа від -5 до 256 і інтернує багато рядкових літералів, схожих на ідентифікатори, як оптимізацію пам'яті й швидкості, тож два окремі літерали можуть посилатися на той самий кешований об'єкт, і `is` випадково поверне True. Це деталь реалізації CPython, а не гарантія мови: поведінка може змінюватися між версіями чи контекстами (наприклад, значення, побудовані під час виконання через конкатенацію чи форматування, зазвичай не інтернуються), тож для порівняння значень слід завжди використовувати `==`, а `is` залишати для перевірки ідентичності синглтонів на кшталт `None`.

Сильна відповідь включає

  • кеш малих чисел і інтернування рядків — деталі реалізації
  • не гарантовано між версіями чи шляхами виконання
  • == для значень, is лише для ідентичності синглтонів

Практичний приклад

`x = 200; y = 200; x is y` часто дає True через кешування цілих чисел, а `x = 2000; y = 2000; x is y` зазвичай дає False, бо 2000 виходить за межі кешованого діапазону.

#cpython-internals#gotchas

Джерела

Data modelPython Software Foundation · Dunder methods, object identity, attribute lookup and the descriptor and context manager protocolsdis — Disassembler for Python bytecodePython Software Foundation · Bytecode execution model used to reason about interpreter performance
Core language and syntaxJuniorOccasionalPractical

Як працюють ланцюгові порівняння в Python і чим вони відрізняються від того самого виразу в більшості інших мов?

Відповідь

Python обчислює `a < b < c` як `a < b and b < c`, причому `b` обчислюється лише один раз, зі скороченим обчисленням (short-circuit) до False, щойно одне з порівнянь не виконується. У багатьох інших мовах `a < b < c` обчислювалося б зліва направо як `(a < b) < c`, порівнюючи булеве значення з `c`, що дає заплутаний і зазвичай безглуздий результат. Варіант Python читається природно, як математична нотація, і є ідіоматичним для перевірки діапазонів.

Сильна відповідь включає

  • еквівалент a < b and b < c
  • середній операнд обчислюється один раз, зі скороченням
  • відрізняється від зліва-направо булевого ланцюжка в інших мовах

Практичний приклад

`if 0 <= age < 18:` читається прямо як перевірка діапазону, еквівалентно `if 0 <= age and age < 18:`, але без подвійного обчислення `age`.

#operators#syntax

Джерела

The Python Language ReferencePython Software Foundation · Precise scoping, evaluation order and expression semantics
Data types and structuresJuniorVery commonTheory

Коли варто обирати tuple замість list?

Відповідь

Обирайте tuple для незмінного запису фіксованого розміру з різнорідними полями — наприклад, пари координат чи рядка з бази даних — де незмінність передає намір і запобігає випадковій зміні, а list — для однорідної колекції змінної довжини, яку плануєте розширювати, скорочувати чи переупорядковувати. Кортежі також хешовані, якщо хешовані їхні елементи, тож їх можна використовувати як ключі словника чи елементи множини, на відміну від списків.

Сильна відповідь включає

  • tuple для незмінних записів фіксованої форми
  • list для колекцій, що ростуть
  • кортежі хешовані й можуть бути ключами словника

Практичний приклад

`point = (12.4, 55.1)` сигналізує про незмінну 2D-координату, форма якої не має змінюватись, тоді як `queue = []` сигналізує про колекцію, до якої додаватимуть і з якої вилучатимуть елементи з часом.

#lists#tuples

Джерела

The Python Standard LibraryPython Software Foundation · Authoritative behavior for built-in types and standard library modulesThe Python TutorialPython Software Foundation · Baseline syntax, control flow, functions and module basics
Data types and structuresMiddleCommonTheory

Чи зберігають словники Python порядок вставки, і як пошук за ключем досягає майже O(1)?

Відповідь

З Python 3.7 збереження порядку вставки є гарантованою можливістю мови, а не деталлю реалізації — ітерація по словнику видає ключі в порядку їх першого додавання. Пошук швидкий, бо словник — це хеш-таблиця: хеш ключа визначає бакет, а рівні ключі повинні мати рівні хеші, тож `__eq__` і `__hash__` мають бути узгодженими для будь-якого власного типу ключа. Середня складність get/set/delete — O(1), хоча патологічний патерн колізій хешів може її погіршити.

Сильна відповідь включає

  • порядок вставки гарантовано з 3.7
  • хеш-таблиця з бакетами за хешем
  • власні ключі потребують узгоджених __eq__ і __hash__

Практичний приклад

Власний клас `UserId`, що використовується як ключ словника, повинен узгоджено визначати `__eq__` і `__hash__`; інакше два логічно рівні екземпляри `UserId` можуть потрапити в різні бакети й ніколи не розпізнаватися як однаковий ключ.

#dict#hashing

Джерела

The Python Standard LibraryPython Software Foundation · Authoritative behavior for built-in types and standard library modulesData modelPython Software Foundation · Dunder methods, object identity, attribute lookup and the descriptor and context manager protocols
Data types and structuresJuniorCommonPractical

Для чого призначена множина (set) у Python, і що роблять оператори об'єднання, перетину та різниці?

Відповідь

Множина зберігає унікальні хешовані елементи без визначеного порядку й дає в середньому O(1) для перевірки належності, що робить її ідеальною для дедуплікації та швидких перевірок `in`. `|` обчислює об'єднання (всі елементи з обох множин), `&` — перетин (елементи в обох), `-` — різницю (елементи в лівій множині, яких немає в правій), а `^` — симетричну різницю (елементи рівно в одній із двох множин).

Сильна відповідь включає

  • унікальні хешовані елементи, швидка перевірка належності
  • оператори об'єднання, перетину, різниці, симетричної різниці
  • добре для дедуплікації

Практичні приклади

Дедуплікація та порівняння наборів ID
existing_ids = {101, 102, 103}
incoming_ids = {102, 103, 104, 105}

print(incoming_ids - existing_ids)
print(incoming_ids & existing_ids)
print(incoming_ids | existing_ids)

Різниця множин знаходить ID лише у вхідному наборі, перетин — ID в обох наборах, а об'єднання поєднує обидва без дублікатів.

Очікуваний результат: {104, 105}; {102, 103}; і одна множина з 101–105 (порядок відображення set не гарантується).

#set#collections

Джерела

The Python Standard LibraryPython Software Foundation · Authoritative behavior for built-in types and standard library modules
Data types and structuresMiddleVery commonTroubleshooting

Яка різниця між поверхневою (shallow) і глибокою (deep) копією, і коли ця різниця реально проявляється як проблема?

Відповідь

Поверхнева копія, зроблена через `list(original)`, `original.copy()` чи `copy.copy()`, створює новий зовнішній контейнер, але все ще посилається на ті самі вкладені об'єкти, що й оригінал. Глибока копія, зроблена через `copy.deepcopy()`, рекурсивно копіює й вкладені об'єкти, використовуючи внутрішній словник-меmo, щоб зберегти спільні посилання й безпечно обробити циклічні структури, тож зміна звичайного вкладеного об'єкта в копії зазвичай більше не впливає на оригінал. Це не абсолютна гарантія повної незалежності: `deepcopy` залишає деякі об'єкти (як функції, класи й модулі) незміненими, а не копіює їх, і клас може налаштувати власну поведінку копіювання через `__deepcopy__`. У будь-якому разі різниця проявляється, коли контейнер містить змінювані вкладені об'єкти: зміна вкладеного списку в поверхневій копії також змінює той самий вкладений список в оригіналі — часте джерело важко відстежуваних помилок.

Сильна відповідь включає

  • поверхнева копія ділить вкладені змінювані об'єкти
  • глибока копія рекурсивно копіює через memo, без обіцянки повної незалежності
  • помилка проявляється при зміні вкладених контейнерів

Практичні приклади

Вкладена зміна показує різницю shallow copy
from copy import deepcopy

original = [[1, 2], [3, 4]]
shallow = original.copy()
deep = deepcopy(original)

shallow[0].append(9)
deep[1].append(8)

print(original)
print(shallow)
print(deep)

Shallow copy створює новий зовнішній список, але ділить внутрішні списки з original. deepcopy рекурсивно створює незалежні вкладені контейнери для цієї звичайної структури списків.

Очікуваний результат: original і shallow обидва містять 9 у першому вкладеному списку; лише deep містить 8 у другому.

#copying#gotchas

Джерела

The Python Standard LibraryPython Software Foundation · Authoritative behavior for built-in types and standard library modules
Data types and structuresMiddleVery commonPractical

Чим генераторний вираз відрізняється від list comprehension, і коли ця різниця має значення?

Відповідь

List comprehension `[x * 2 for x in data]` одразу будує весь список у пам'яті. Генераторний вираз `(x * 2 for x in data)` будує лінивий ітератор, що видає по одному значенню за запитом і ніколи не матеріалізується як повна послідовність. Різниця важлива для великих чи необмежених джерел даних: генератор уникає матеріалізації всієї результуючої послідовності одразу, що зазвичай тримає його власний слід пам'яті набагато меншим, ніж у list comprehension, хоча загальне використання пам'яті все ще залежить від того, що код навколо робить з кожним значенням і чи накопичується стан генератора. Генератор також можна пройти лише один раз, і він не підтримує індексацію чи `len()`.

Сильна відповідь включає

  • list comprehension — жадібний, генераторний вираз — лінивий
  • уникає матеріалізації всієї послідовності, зазвичай набагато менша пам'ять за список
  • генератори одноразові, без індексації чи len

Практичні приклади

Жадібний list проти лінивого generator
squares_list = [n * n for n in range(5)]
squares_gen = (n * n for n in range(5))

print(squares_list)
print(next(squares_gen))
print(next(squares_gen))
print(list(squares_gen))

Comprehension одразу матеріалізує всі п'ять значень. Генератор створює значення лише під час споживання й пам'ятає місце зупинки між next().

Очікуваний результат: [0, 1, 4, 9, 16], потім 0, потім 1, потім [4, 9, 16].

#comprehensions#generators#performance

Джерела

The Python TutorialPython Software Foundation · Baseline syntax, control flow, functions and module basicsitertools — Functions creating iterators for efficient loopingPython Software Foundation · Lazy iterator composition and memory-efficient looping patterns
Data types and structuresJuniorCommonPractical

Як працює зрізання (slicing) `start:stop:step` в Python, включно з від'ємними індексами та від'ємним кроком?

Відповідь

Зріз `sequence[start:stop:step]` для вбудованих послідовностей на кшталт `list`, `str` і `tuple` повертає нову послідовність того самого типу з елементів від індексу `start` до, але не включно, `stop`, з кроком `step`; точний тип результату й поведінка для конкретного об'єкта врешті визначається власним `__getitem__` цього типу, тож власний чи сторонній тип послідовності міг би повернути щось інше за звичайну копію, наприклад представлення (view). Від'ємні індекси рахуються з кінця (`-1` — останній елемент), а від'ємний крок змінює напрям, тож `stop` логічно має йти перед `start` у порядку ітерації. Для вбудованих типів послідовностей зрізання ніколи не викликає IndexError для індексів поза межами — воно просто обрізає їх, що робить його безпечним для приблизних діапазонів.

Сильна відповідь включає

  • stop не включається у результат
  • від'ємні індекси рахуються з кінця
  • для вбудованих послідовностей межі обрізаються без помилки; власний __getitem__ може поводитись інакше

Практичний приклад

`"hello world"[::-1]` перевертає рядок у `"dlrow olleh"`, а `values[-3:]` безпечно повертає останні три елементи, навіть якщо у `values` менше трьох елементів.

#slicing#sequences

Джерела

The Python Language ReferencePython Software Foundation · Precise scoping, evaluation order and expression semanticsThe Python TutorialPython Software Foundation · Baseline syntax, control flow, functions and module basics
Data types and structuresMiddleCommonTroubleshooting

Чому `grid = [[0] * 3] * 3` створює зіпсовану сітку 3x3, і як правильно побудувати вкладений список?

Відповідь

`[[0] * 3] * 3` спочатку створює один внутрішній список `[0, 0, 0]`, а потім зовнішній `* 3` тричі повторює посилання саме на цей самий об'єкт внутрішнього списку, тож усі три рядки псевдоніми одне одного, і зміна одного рядка змінює всі. Виправлення — будувати кожен рядок незалежно, зазвичай через comprehension: `grid = [[0] * 3 for _ in range(3)]`, що обчислює `[0] * 3` заново на кожній ітерації й дає три окремі об'єкти списку.

Сильна відповідь включає

  • * на списку повторює посилання, а не копії
  • усі рядки — псевдоніми одного внутрішнього списку
  • виправлення через comprehension, що будує кожен рядок окремо

Практичний приклад

Після `grid = [[0] * 3] * 3` виконання `grid[0][0] = 1` змінює `grid` на `[[1, 0, 0], [1, 0, 0], [1, 0, 0]]`, бо кожен рядок — той самий об'єкт.

#lists#aliasing#gotchas

Джерела

The Python Standard LibraryPython Software Foundation · Authoritative behavior for built-in types and standard library modulesThe Python TutorialPython Software Foundation · Baseline syntax, control flow, functions and module basics
Data types and structuresSeniorOccasionalTroubleshooting

Кортеж незмінюваний, тож чому `t = ([1, 2], 3); t[0].append(4)` виконується без помилки?

Відповідь

Незмінюваність кортежу захищає лише самі прив'язки кортежу: які об'єкти стоять на кожній позиції, ніколи не зміниться після створення. Це нічого не каже про змінюваність об'єктів, на які посилаються ці позиції. Якщо позиція містить змінюваний об'єкт, як список, його все ще можна змінити на місці через власні методи; лише повторне присвоєння самій позиції (`t[0] = something`) видасть TypeError. Це також означає, що хешованість кортежу залежить від того, чи хешований кожен його елемент, а не безпосередньо від змінюваності: `([1, 2], 3)` нехешований саме тому, що `list` нехешований, тож не може бути ключем словника чи елементом множини, тоді як кортеж лише з хешованих елементів залишається хешованим, навіть якщо один з елементів — власний змінюваний об'єкт, що визначає `__hash__`.

Сильна відповідь включає

  • незмінюваність захищає прив'язки позицій, а не вміст об'єктів
  • мутація на місці змінюваного елемента все ще працює
  • хешованість кортежу залежить від хешованості кожного елемента, не від змінюваності напряму

Практичний приклад

`t[0].append(4)` змінює на місці список, збережений за індексом 0, і виконується успішно, а `t[0] = [9]` викликає `TypeError: 'tuple' object does not support item assignment`.

#tuples#mutability#gotchas

Джерела

Data modelPython Software Foundation · Dunder methods, object identity, attribute lookup and the descriptor and context manager protocols
Data types and structuresMiddleCommonDesign

Як обрати між list, set і dict для коду, де важливий частий пошук?

Відповідь

Перевірка належності (`x in container`) має складність O(n) для списку, бо він сканується елемент за елементом, але в середньому O(1) для множини чи словника, бо обидва використовують хешування. Якщо потрібно часто перевіряти належність і не потрібен порядок чи пари ключ-значення, правильна структура — set. Якщо потрібно пов'язати з кожним ключем додаткові дані, dict дає той самий O(1) пошук плюс це пов'язане значення. List залишається правильним вибором, коли порядок і повторювані значення важливіші за швидкість пошуку.

Сильна відповідь включає

  • перевірка належності в списку — O(n), у множині/словнику — в середньому O(1)
  • set для чистої належності, dict для пошуку ключ-значення
  • list, коли порядок і дублікати важливіші за швидкість

Практичний приклад

Заміна `if user_id in list_of_ids:` всередині циклу на `if user_id in set_of_ids:` може перетворити O(n²) прохід валідації на O(n) для великого набору даних.

#performance#data-structures#design

Джерела

The Python Standard LibraryPython Software Foundation · Authoritative behavior for built-in types and standard library modules
Data types and structuresMiddleOccasionalPractical

Які сучасні способи об'єднання двох словників є в Python, і як вони поводяться з ключами, що збігаються?

Відповідь

У Python 3.9 додано оператор об'єднання `|` та оператор об'єднання на місці `|=` для словників, що дає лаконічний вираз `merged = defaults | overrides`, який повертає новий словник. Старіші ідіоми — розпаковка `{**defaults, **overrides}` та `defaults.copy(); result.update(overrides)`. У всіх випадках, коли обидва словники визначають однаковий ключ, перемагає значення з правого чи пізніше застосованого словника.

Сильна відповідь включає

  • оператори об'єднання | та |= з 3.9
  • розпаковка з подвійною зіркою як старіший еквівалент
  • при колізії ключів перемагає правий/пізніший словник

Практичний приклад

`config = defaults | user_overrides` створює новий словник, де будь-який ключ, наявний в обох, отримує значення з `user_overrides`.

#dict#syntax

Джерела

The Python Standard LibraryPython Software Foundation · Authoritative behavior for built-in types and standard library modulesWhat's new in Python 3.14Python Software Foundation · Current-version language and runtime changes, including template strings and free-threading support
Data types and structuresMiddleOccasionalTheory

Що таке frozenset, і навіщо використовувати його замість set?

Відповідь

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

Сильна відповідь включає

  • незмінюваний, хешований варіант set
  • можна використовувати як ключ словника чи елемент множини
  • підходить для фіксованих колекцій-ключів пошуку

Практичний приклад

`cache = {}` з ключами `frozenset(active_flags)` дозволяє шару мемоізації розглядати `{"a", "b"}` і `{"b", "a"}` як той самий ключ кешу незалежно від порядку додавання.

#set#hashing#immutability

Джерела

The Python Standard LibraryPython Software Foundation · Authoritative behavior for built-in types and standard library modules
Data types and structuresJuniorCommonPractical

Як працює аргумент `key` у `sorted()` та `list.sort()`, і чому він кращий за власну функцію порівняння?

Відповідь

Аргумент `key` приймає функцію, яка викликається один раз для кожного елемента, щоб обчислити ключ сортування, а потім елементи впорядковуються за порівнянням цих обчислених ключів, а не самих елементів. Це кращий підхід за старі функції порівняння, бо функція ключа викликається лише n разів замість O(n log n) порівнянь пар, добре поєднується з `reverse=True` й легко читається: `sorted(people, key=lambda p: p.age)` очевидно сортує за віком.

Сильна відповідь включає

  • функція ключа обчислюється раз на елемент
  • швидше за попарну функцію порівняння
  • поєднується з reverse=True й легко читається

Практичний приклад

`sorted(products, key=lambda p: (-p.rating, p.price))` сортує спершу за найвищим рейтингом, а потім за найнижчою ціною серед товарів з однаковим рейтингом, використовуючи ключ-кортеж.

#sorting#lambda

Джерела

The Python Standard LibraryPython Software Foundation · Authoritative behavior for built-in types and standard library modulesThe Python TutorialPython Software Foundation · Baseline syntax, control flow, functions and module basics
Strings and text processingJuniorVery commonTheory

Чому рядки в Python незмінювані, і що це означає для методів на кшталт `.upper()` чи `.replace()`?

Відповідь

Рядки незмінювані, щоб їх можна було безпечно хешувати й ефективно ділити між змінними без захисного копіювання, і щоб після створення вміст рядка ніколи не міг бути тихо змінений кодом деінде, що тримає те саме посилання. Кожен метод рядка, що виглядає так, ніби «змінює» рядок, як-от `.upper()`, `.replace()` чи `.strip()`, ніколи не змінює оригінальний рядок на місці — він повертає рядок з результатом, а вміст оригіналу впливається лише якщо змінній повторно присвоїти цей новий результат. (Чи є повернутий об'єкт свіжо виділеним, чи, у граничних випадках на кшталт перетворення без змін, тим самим об'єктом — деталь реалізації, яку Python не гарантує в жоден бік; гарантія лише в тому, що оригінал ніколи не змінюється.)

Сильна відповідь включає

  • незмінюваність дозволяє безпечне хешування й спільне використання
  • методи рядків ніколи не змінюють оригінал, лише повертають результат
  • оригінал не змінюється без повторного присвоєння

Практичний приклад

`name = "ann"; name.upper()` обчислюється в `"ANN"`, але `name` залишається `"ann"`, якщо не написати `name = name.upper()`.

#strings#immutability

Джерела

The Python Standard LibraryPython Software Foundation · Authoritative behavior for built-in types and standard library modulesData modelPython Software Foundation · Dunder methods, object identity, attribute lookup and the descriptor and context manager protocols
Strings and text processingJuniorVery commonPractical

Які три основні способи форматування рядків є в Python, і чому f-рядки тепер надаються перевага?

Відповідь

Три підходи: старе форматування у стилі `%` (`"%s is %d" % (name, age)`), метод `.format()` (`"{} is {}".format(name, age)`) та f-рядки (`f"{name} is {age}"`), введені в Python 3.6. F-рядкам надають перевагу, бо вирази обчислюються прямо там, де написані, що читабельніше, вони підтримують ту саму міні-мову специфікації формату, що й `.format()`, і можуть викликати методи й навіть вкладені вирази безпосередньо. F-рядки також зазвичай найшвидші з трьох у типових бенчмарках CPython, але цей розрив залежить від версії Python і того, що саме форматується, тож читабельність — більш стійка причина надавати їм перевагу.

Сильна відповідь включає

  • форматування %, .format() та f-рядки
  • f-рядки вбудовують вирази прямо в текст
  • f-рядки з'явились у 3.6, зазвичай найшвидші в бенчмарках, але це не гарантія мови

Практичний приклад

`f"{price:,.2f}"` форматує число з плаваючою комою з розділювачами тисяч і двома знаками після коми прямо в літералі рядка, наприклад перетворює `1234.5` на `"1,234.50"`.

#strings#formatting

Джерела

The Python TutorialPython Software Foundation · Baseline syntax, control flow, functions and module basicsWhat's new in Python 3.14Python Software Foundation · Current-version language and runtime changes, including template strings and free-threading support
Strings and text processingMiddleCommonPerformance

Чому `"".join(parts)` рекомендують замість повторної конкатенації `+=` при побудові великого рядка з багатьох частин?

Відповідь

Оскільки рядки незмінювані, кожен `result += piece` у циклі концептуально має виділити новий рядок і скопіювати в нього старий вміст плюс нову частину, що робить наївну повторну конкатенацію патерном O(n²) для n частин у загальному випадку. `"".join(parts)` спочатку один раз проходить список, щоб обчислити загальну довжину, виділяє єдиний буфер точно потрібного розміру й копіює кожну частину в нього один раз, що робить всю операцію O(n). На практиці CPython має внутрішню оптимізацію, що іноді може розширювати рядок на місці, коли змінна циклу тримає єдине посилання на нього, але ця оптимізація — деталь реалізації CPython, а не гарантія мови, тож `"".join()` залишається передбачуваним, рекомендованим підходом при об'єднанні багатьох частин.

Сильна відповідь включає

  • += у циклі наївно перевиділяє й копіює на кожній ітерації, O(n²) у загальному випадку
  • join заздалегідь обчислює довжину й виділяє пам'ять один раз
  • оптимізація CPython на місці — деталь реалізації, не гарантія мови; join залишається передбачуваним

Практичний приклад

Побудова звіту зі 100 000 рядків через `report += line` у циклі помітно повільніша за збір рядків у список і виклик `"\n".join(lines)` один раз у кінці.

#strings#performance

Джерела

The Python Standard LibraryPython Software Foundation · Authoritative behavior for built-in types and standard library modules
Strings and text processingMiddleCommonPractical

Яка різниця між `str` і `bytes` у Python 3, і коли потрібно `.encode()` чи `.decode()`?

Відповідь

`str` зберігає послідовність кодових точок Unicode — абстрактний текст без фіксованого байтового представлення, тоді як `bytes` зберігає послідовність сирих 8-бітних значень, саме те, що реально передають файли, сокети й більшість зовнішніх систем. `.encode("utf-8")` перетворює `str` на `bytes`, використовуючи обране кодування, а `.decode("utf-8")` перетворює `bytes` назад на `str`, інтерпретуючи сирі байти згідно з тим самим кодуванням. Перетворення `str`/`bytes` потрібне саме там, де текст перетинає межу текст/бінарні дані — наприклад, передача рядка сокету чи інтерпретація сирих байтів як тексту; самі бінарні файли й сокети оперують чистими `bytes` без жодного неявного кодування, тоді як текстові обгортки файлів Python (`open(path)` без `"b"`) виконують цей крок encode/decode за вас автоматично, використовуючи типове для платформи кодування, якщо не вказати інше, що може виявитися неправильним для застарілих даних.

Сильна відповідь включає

  • str — абстрактний Unicode-текст, bytes — сирі 8-бітні дані
  • encode перетворює str на bytes, decode — bytes на str
  • бінарні файли/сокети несуть сирі байти без неявного кодування; текстові обгортки роблять encode/decode

Практичний приклад

`"café".encode("utf-8")` дає `b'caf\xc3\xa9'`, бо é займає два байти в UTF-8; декодування цих байтів неправильним кодеком, наприклад `"latin-1"`, тихо дає спотворений текст замість помилки.

#strings#encoding#bytes

Джерела

The Python Standard LibraryPython Software Foundation · Authoritative behavior for built-in types and standard library modulesThe Python TutorialPython Software Foundation · Baseline syntax, control flow, functions and module basics
Strings and text processingMiddleCommonPractical

Яка різниця між `re.match`, `re.search` та `re.fullmatch`, і чому регулярні вирази зазвичай варто компілювати?

Відповідь

`re.match` перевіряє збіг, лише прив'язаний до самого початку рядка, `re.search` сканує весь рядок у пошуку першого місця, де патерн збігається будь-де, а `re.fullmatch` вимагає, щоб увесь рядок збігався з патерном від початку до кінця. `re.compile(pattern)` попередньо розбирає патерн у повторно використовуваний об'єкт `Pattern`; головна перевага — явне повторне використання й читабельність: передавати скомпільований об'єкт і платити за розбір один раз у чіткому місці коду, плюс невелика гарантована економія пошуку порівняно з викликом `re.match(pattern, ...)` із сирим рядком щоразу. Модуль `re` також внутрішньо кешує обмежену кількість нещодавно використаних скомпільованих патернів, тож повторні виклики функцій рівня модуля з тим самим рядком патерну не обов'язково розбирають його заново щоразу — але цей кеш є деталлю реалізації з обмеженим розміром, тож явний `compile()` залишається надійним вибором у циклі, гарячому шляху валідації чи коли задіяно багато різних патернів.

Сильна відповідь включає

  • match прив'язаний до початку, search сканує будь-де, fullmatch вимагає весь рядок
  • головна перевага compile — явне повторне використання й читабельність, не лише уникнення розбору
  • re також кешує останні патерни внутрішньо, але цей кеш обмежений і є деталлю реалізації

Практичний приклад

`re.match("\\d+", "abc123")` повертає None, бо цифри не починаються з позиції 0, тоді як `re.search("\\d+", "abc123")` знаходить `"123"`, починаючи з позиції 3.

#regex#strings

Джерела

The Python Standard LibraryPython Software Foundation · Authoritative behavior for built-in types and standard library modules
Strings and text processingJuniorVery commonPractical

Що роблять `strip()`, `split()` та `join()`, і яка поширена помилка з поведінкою `split()` за замовчуванням?

Відповідь

`strip()` видаляє пробіли на початку й в кінці (або задані символи), не чіпаючи середину рядка. `split()` розбиває рядок на список за роздільником, а без аргументу розбиває за будь-якою послідовністю пробільних символів і відкидає порожні рядки від початкових/кінцевих/повторюваних роздільників, що дивує тих, хто очікує простого розбиття за одним пробілом. `join()` — це обернена дія до `split()`: викликається на рядку-роздільнику й склеює ітерований набір рядків цим роздільником між частинами.

Сильна відповідь включає

  • strip обрізає краї, не середину
  • split() без аргументу розбиває за будь-якими пробілами й відкидає порожні частини
  • join викликається на роздільнику, а не на списку

Практичний приклад

`" a b c ".split()` повертає `['a', 'b', 'c']`, охайно згортаючи всі пробіли, тоді як `" a b c ".split(" ")` повертає `['', '', 'a', '', '', 'b', 'c', '']`, зберігаючи кожен порожній проміжок.

#strings#gotchas

Джерела

The Python Standard LibraryPython Software Foundation · Authoritative behavior for built-in types and standard library modules
Strings and text processingJuniorOccasionalPractical

Що робить префікс `r` для рядкового літералу, і чому його часто використовують з регулярними виразами та шляхами Windows?

Відповідь

Сирий (raw) рядковий літерал, написаний з префіксом `r`, наприклад `r"\d+"`, каже Python не інтерпретувати керуючі послідовності з бекслешем, як-от `\n` чи `\t`; кожен бекслеш зберігається як звичайний символ. Це важливо для регулярних виразів, які широко використовують бекслеші у власному синтаксисі екранування (`\d`, `\s`, `\w`), бо звичайний рядок вимагав би подвоєння кожного бекслеша (`"\\d+"`), щоб уникнути колізії власної обробки екранування Python з екрануванням регекс-рушія. Це також зручно для шляхів Windows на кшталт `r"C:\Users\name"`, уникаючи випадкових керуючих послідовностей на кшталт `\U` чи `\n` всередині шляху.

Сильна відповідь включає

  • префікс r вимикає інтерпретацію керуючих послідовностей
  • уникає подвійного екранування бекслешів у регексах
  • також зручно для шляхів у стилі Windows

Практичний приклад

`r"C:\new_folder"` зберігає бекслеші буквально, тоді як не-сирий `"C:\new_folder"` неправильно інтерпретує `\n` як символ нового рядка, даючи `"C:" + новий рядок + "ew_folder"`.

#strings#regex#syntax

Джерела

The Python TutorialPython Software Foundation · Baseline syntax, control flow, functions and module basicsThe Python Language ReferencePython Software Foundation · Precise scoping, evaluation order and expression semantics
Strings and text processingSeniorSpecialistTheory

Що таке рядкові літерали-шаблони (t-рядки), введені в Python 3.14, і чим вони відрізняються від f-рядків?

Відповідь

T-рядок, записаний як `t"Hello {name}"`, використовує той самий синтаксис інтерполяції, що й f-рядок, але замість того, щоб одразу давати готовий `str`, обчислюється в об'єкт `Template`, що зберігає статичний текст та інтерпольовані значення як окремі, доступні для інспекції частини. Це дозволяє функції-споживачу вирішувати, як обробити кожне інтерпольоване значення перед складанням фінального результату. Сам по собі t-рядок не є автоматично безпечним — це не санітизатор, — але його структура є корисним будівельним блоком для помічників, яким потрібно інспектувати, екранувати чи маскувати інтерпольовані значення, наприклад помічника маскування в логах, чи коду, що врешті делегує SQL власному API параметризованих запитів драйвера бази даних, а не будує SQL конкатенацією рядків. F-рядок натомість вже сплющив усе в один непрозорий рядок до того, як будь-який код зможе його інспектувати.

Сильна відповідь включає

  • той самий синтаксис {}, що й у f-рядків, новий у 3.14
  • обчислюється в об'єкт Template, а не одразу str
  • дозволяє інспекцію частин перед рендерингом, але сам по собі не робить SQL чи подібне автоматично безпечним

Практичний приклад

Помічник логування може приймати t-рядок і екранувати чи маскувати кожне інтерпольоване поле окремо перед форматуванням фінального рядка логу — те, що f-рядок підтримати не може, бо значення вже злиті у звичайний текст.

#strings#python-3-14

Джерела

What's new in Python 3.14Python Software Foundation · Current-version language and runtime changes, including template strings and free-threading support
Strings and text processingMiddleOccasionalPractical

Як працює міні-мова специфікації формату всередині f-рядка чи виклику `.format()`, наприклад `f"{value:>10.2f}"`?

Відповідь

Частина після двокрапки всередині `{...}` — це міні-мова із загальною формою `[[fill]align][sign][width][,][.precision][type]`. `align` керує вирівнюванням ліворуч (`<`), праворуч (`>`) або по центру (`^`) в межах `width` символів, `,` чи `_` вставляє розділювачі тисяч, `.precision` керує кількістю знаків після коми для чисел з плаваючою комою чи максимальною довжиною для рядків, а `type` обирає представлення, наприклад `f` для фіксованої точки, `%` для відсотка, `d` для цілого числа чи `x` для шістнадцяткового.

Сильна відповідь включає

  • загальна форма [[fill]align][sign][width][,][.precision][type]
  • вирівнювання, ширина, роздільник тисяч, точність, тип представлення
  • та сама специфікація працює у f-рядках і .format()

Практичний приклад

`f"{0.5:>7.1%}"` дає `" 50.0%"`: 0.5 трактується як відсоток, показується один знак після коми, а результат вирівнюється праворуч у полі шириною 7 символів.

#strings#formatting

Джерела

The Python Standard LibraryPython Software Foundation · Authoritative behavior for built-in types and standard library modules
Strings and text processingJuniorCommonPractical

Як потрійні лапки пов'язані з docstring, і як docstring отримують під час виконання?

Відповідь

Рядок у потрійних лапках (`"""..."""` чи `'''...'''`) дозволяє рядковому літералу займати декілька рядків і містити вбудовані символи лапок без екранування. Docstring, за визначенням, — це будь-який рядковий літерал-вираз, що є найпершим оператором усередині модуля, класу, функції чи методу — однорядковий рядок у звичайних лапках на цій позиції теж технічно docstring; потрійні лапки — просто звичайний, прийнятий стиль, бо добре читаються для багаторядкової документації. Python автоматично зберігає той рядковий літерал, що виконує цю роль, в атрибуті `__doc__` цього об'єкта, а інструменти на кшталт `help()`, IDE та генератори документації читають `__doc__`, щоб показати інформацію про використання без окремих файлів документації.

Сильна відповідь включає

  • потрійні лапки дозволяють багаторядкові літерали з вбудованими лапками
  • docstring визначається синтаксичною позицією — перший оператор блоку, — а не самими потрійними лапками
  • автоматично зберігається в __doc__, читається help() та інструментами документації

Практичний приклад

`def add(a, b):\n """Return the sum of a and b."""\n return a + b` робить `add.__doc__` рівним `"Return the sum of a and b."`, доступним під час виконання через `help(add)`.

#strings#docstrings

Джерела

The Python TutorialPython Software Foundation · Baseline syntax, control flow, functions and module basicsThe Python Language ReferencePython Software Foundation · Precise scoping, evaluation order and expression semantics
Functions and functional toolsJuniorVery commonPractical

Що означають `*args` і `**kwargs` у сигнатурі функції?

Відповідь

`*args` збирає будь-які додаткові позиційні аргументи, які передав викликач понад іменовані параметри, у кортеж, а `**kwargs` збирає будь-які додаткові іменовані аргументи у словник. Імена `args` і `kwargs` — лише угода, а не вимога синтаксису; значення мають саме префікси `*` і `**`. Вони дозволяють функції приймати гнучку, змінну кількість аргументів і часто використовуються для функцій-обгорток і декораторів, що мають передавати отримані аргументи іншому викличному об'єкту.

Сильна відповідь включає

  • *args збирає зайві позиційні у кортеж
  • **kwargs збирає зайві іменовані у словник
  • використовується для передачі довільних аргументів, наприклад у декораторах

Практичні приклади

Передача довільної сигнатури виклику
def trace(func):
    def wrapper(*args, **kwargs):
        print("args:", args)
        print("kwargs:", kwargs)
        return func(*args, **kwargs)
    return wrapper

@trace
def greet(name, punctuation="!"):
    return f"Hello, {name}{punctuation}"

print(greet("Ada", punctuation="."))

*args збирає позиційні значення в tuple, а **kwargs — іменовані в dict. Передача обох зберігає гнучкість виклику обгорнутої функції.

Очікуваний результат: Wrapper друкує ('Ada',), потім {'punctuation': '.'}, потім Hello, Ada.

#functions#args-kwargs

Джерела

The Python TutorialPython Software Foundation · Baseline syntax, control flow, functions and module basicsThe Python Language ReferencePython Software Foundation · Precise scoping, evaluation order and expression semantics
Functions and functional toolsMiddleCommonTroubleshooting

Коли обчислюються значення аргументів за замовчуванням, і яку проблему це створює для значення на кшталт `datetime.now()`?

Відповідь

Вирази значень за замовчуванням обчислюються рівно один раз, у момент виконання оператора `def` і створення об'єкта функції, а не при кожному виклику. Це означає, що `def log(message, timestamp=datetime.now())` фіксує одну незмінну мітку часу — момент імпорту модуля, і кожен виклик `log()` без переданого `timestamp` назавжди повторно використовує це саме застаріле значення. Виправлення — задати за замовчуванням `None` і обчислювати реальне значення всередині тіла функції: `def log(message, timestamp=None): timestamp = timestamp or datetime.now()`.

Сильна відповідь включає

  • значення за замовчуванням обчислюються один раз при визначенні, не при кожному виклику
  • datetime.now() за замовчуванням фіксується в момент імпорту
  • виправлення: None за замовчуванням, обчислення всередині функції

Практичні приклади

Обчислення динамічних значень за замовчуванням під час виклику
from datetime import datetime

def stamp_bad(created_at=datetime.now()):
    return created_at

def stamp_good(created_at=None):
    if created_at is None:
        created_at = datetime.now()
    return created_at

print(stamp_bad() is stamp_bad())
print(stamp_good() == stamp_good())

Погане значення за замовчуванням обчислюється один раз при виконанні def. None слугує sentinel, щоб безпечна версія викликала datetime.now() під час кожного виклику функції.

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

#functions#gotchas

Джерела

The Python Language ReferencePython Software Foundation · Precise scoping, evaluation order and expression semanticsThe Python TutorialPython Software Foundation · Baseline syntax, control flow, functions and module basics
Functions and functional toolsMiddleCommonTheory

Що таке замикання (closure) в Python, і чому замикання, створене всередині циклу, часто захоплює неправильне значення?

Відповідь

Замикання — це внутрішня функція, що пам'ятає й може звертатися до змінних з області видимості охоплюючої функції навіть після її завершення, бо Python тримає ці змінні живими в спільній «комірці» (cell), на яку посилається внутрішня функція. Усередині циклу, якщо lambda чи вкладена функція звертається до змінної циклу напряму, кожне замикання, створене в цьому циклі, ділить ту саму комірку, тож на момент реального виклику будь-якого із замикань усі вони бачать кінцеве значення змінної циклу, а не те значення, яке вона мала на момент створення кожного замикання.

Сильна відповідь включає

  • внутрішня функція зберігає доступ до охоплюючої області через спільну комірку
  • замикання в циклі ділять одну комірку для змінної циклу
  • усі замикання бачать кінцеве значення змінної циклу без явної фіксації

Практичні приклади

Late binding у замиканнях, створених у циклі
bad = [lambda: i for i in range(3)]
good = [lambda i=i: i for i in range(3)]

print([fn() for fn in bad])
print([fn() for fn in good])

Погані lambda ділять одну closure-комірку для i й пізніше читають її фінальне значення. Фіксація i як аргументу за замовчуванням зберігає поточне значення під час створення кожної lambda.

Очікуваний результат: [2, 2, 2], потім [0, 1, 2].

#closures#gotchas#lambda

Джерела

The Python Language ReferencePython Software Foundation · Precise scoping, evaluation order and expression semantics
Functions and functional toolsJuniorCommonTheory

Чим вираз `lambda` відрізняється від звичайної функції, визначеної через `def`, і коли варто уникати lambda?

Відповідь

`lambda` — це анонімна функція, тіло якої обмежене одним виразом, а не блоком операторів: вона не може містити цикли `for`/`while`, оператори `if` чи оператори присвоєння, і неявно повертає значення цього єдиного виразу — хоча сам вираз усе ще може використовувати конструкції рівня виразу, як умовний вираз (`a if cond else b`), comprehension чи оператор моржа `:=`, бо це вирази, а не оператори. Функція `def` може містити кілька операторів, docstring, анотації типів і справжнє `__name__`. Надавайте перевагу `def`, коли логіка потребує більшого за один вираз, потребує docstring для підтримуваності, або коли її все одно присвоюють імені — PEP 8 явно не рекомендує прив'язувати lambda до імені замість використання `def`.

Сильна відповідь включає

  • тіло lambda — єдиний вираз, а не блок операторів
  • def підтримує кілька операторів, docstring і справжнє ім'я
  • PEP 8 не рекомендує присвоювати lambda імені

Практичні приклади

Lambda лише для коротких inline-виразів
items = [
    {"name": "slow", "duration": 8},
    {"name": "fast", "duration": 2},
]

ordered = sorted(items, key=lambda item: item["duration"])

def duration(item):
    """Named helper when logic deserves reuse or explanation."""
    return item["duration"]

print(ordered)
print(sorted(items, key=duration))

Lambda зручна для одноразового короткого виразу, наприклад sort key. Іменований def зрозуміліший, коли логіка потребує документації, повторного використання, анотацій чи кількох операторів.

Очікуваний результат: Обидва сортування ставлять елемент з duration 2 перед duration 8.

#lambda#functions#pep-8

Джерела

PEP 8 — Style Guide for Python CodePython Software Foundation · Naming, layout and readability conventionsThe Python TutorialPython Software Foundation · Baseline syntax, control flow, functions and module basics
Functions and functional toolsSeniorOccasionalTheory

Що роблять маркери `/` і `*` у сигнатурі функції на кшталт `def f(a, b, /, c, *, d):`?

Відповідь

Параметри перед голим `/` є позиційно-лише: викликачі мають передавати їх за позицією і ніколи не можуть використати їхнє ім'я параметра як іменований аргумент. Параметри після голого `*` є іменовано-лише: викликачі мають передавати їх за іменем і ніколи не можуть покладатися на позицію. У `def f(a, b, /, c, *, d):` параметри `a` і `b` — позиційно-лише, `c` можна передати обома способами, а `d` обов'язково має бути `d=value`. Це дозволяє дизайнеру API зафіксувати стабільну угоду виклику, приховати внутрішні імена параметрів від публічного синтаксису виклику й уникнути випадкового переплутування позицій для неочевидних аргументів.

Сильна відповідь включає

  • параметри перед / — позиційно-лише
  • параметри після * — іменовано-лише
  • дозволяє зафіксувати угоду виклику API та уникнути випадкового переплутування позицій

Практичний приклад

`def divmod_safe(a, b, /, *, allow_zero=False):` вимагає форму виклику `divmod_safe(10, 3)` і відхиляє `divmod_safe(a=10, b=3)`, водночас вимагаючи явного запису `allow_zero=True`.

#functions#api-design

Джерела

The Python Language ReferencePython Software Foundation · Precise scoping, evaluation order and expression semanticsWhat's new in Python 3.14Python Software Foundation · Current-version language and runtime changes, including template strings and free-threading support
Functions and functional toolsMiddleCommonTheory

Що означає, що функції — об'єкти першого класу в Python, і що таке функція вищого порядку?

Відповідь

Бути об'єктом першого класу означає, що функція — звичайний об'єкт, як і будь-яке інше значення: її можна присвоїти змінній, зберегти в списку чи словнику, передати як аргумент, повернути з іншої функції та перевірити її атрибути на кшталт `__name__` і `__doc__`. Функція вищого порядку — це будь-яка функція, що приймає іншу функцію як аргумент, повертає функцію, або обидва варіанти одразу — `map()`, `sorted(key=...)` та декоратори є функціями вищого порядку, що напряму спираються на функції як об'єкти першого класу.

Сильна відповідь включає

  • функції можна присвоювати, зберігати, передавати й повертати як будь-яке значення
  • функція вищого порядку приймає і/або повертає функцію
  • декоратори, map, sorted(key=) — конкретні приклади

Практичний приклад

`operations = {"add": lambda a, b: a + b, "sub": lambda a, b: a - b}` зберігає функції як значення словника й шукає потрібну за іменем під час виконання через `operations["add"](2, 3)`.

#functions#functional-programming

Джерела

The Python Language ReferencePython Software Foundation · Precise scoping, evaluation order and expression semantics
Functions and functional toolsJuniorCommonPractical

Як `map()` і `filter()` порівнюються з еквівалентним list comprehension, і чому більшість посібників зі стилю тепер надають перевагу comprehension?

Відповідь

`map(func, iterable)` ліниво застосовує `func` до кожного елемента, а `filter(predicate, iterable)` ліниво залишає лише елементи, для яких `predicate` повертає істину; обидва повертають ітератор, а не список. Еквівалентні comprehension, `[func(x) for x in iterable]` та `[x for x in iterable if predicate(x)]`, зазвичай надаються перевазі в сучасному Python, бо читаються зліва направо як звичайний текст, у більшості випадків уникають зайвої `lambda` і можуть поєднувати відображення й фільтрацію в одному виразі, тоді як ланцюжок `map` і `filter` вимагає вкладених викликів.

Сильна відповідь включає

  • map/filter ліниві й повертають ітератори
  • еквівалентний comprehension читається природніше
  • comprehension поєднує відображення й фільтрацію в одному виразі

Практичний приклад

`[x * 2 for x in values if x > 0]` замінює незграбніший `list(map(lambda x: x * 2, filter(lambda x: x > 0, values)))` одним читабельним рядком.

#functional-programming#comprehensions

Джерела

The Python Standard LibraryPython Software Foundation · Authoritative behavior for built-in types and standard library modulesPEP 8 — Style Guide for Python CodePython Software Foundation · Naming, layout and readability conventions
Functions and functional toolsMiddleOccasionalTroubleshooting

Чому глибока рекурсія може призвести до аварійного завершення програми Python, і чи оптимізує Python хвостову рекурсію?

Відповідь

CPython накладає типовий ліміт рекурсії, доступний через `sys.getrecursionlimit()` (зазвичай 1000), і перевищення викликає `RecursionError` як запобіжник. З CPython 3.11 більшість викликів Python-у-Python більше не споживають реальний кадр стека C так, як раніше, — інтерпретатор вбудовує багато таких викликів у власну обробку кадрів, — але захист глибини рекурсії все ще діє, і достатньо глибока чи важка для стека C рекурсія (наприклад, та, що перетинається з кодом на C) все ще може вичерпати реальний системний стек у деяких ситуаціях, тож ліміт залишається справжнім механізмом безпеки, а не залишковим артефактом. На відміну від деяких функціональних мов, CPython не виконує оптимізацію хвостових викликів: рекурсивний виклик у хвостовій позиції не перетворюється на цикл, тож глибоко рекурсивний алгоритм для необмежених вхідних даних зазвичай варто переписати ітеративно чи з явним стеком, а не покладатися на те, що інтерпретатор його сплющить.

Сильна відповідь включає

  • типовий ліміт рекурсії близько 1000, викликає RecursionError як запобіжник
  • з 3.11 більшість викликів Python-у-Python більше не споживають реальний кадр стека C, але захист глибини й частина ризику лишаються
  • CPython не виконує оптимізацію хвостових викликів

Практичний приклад

Наївна `def sum_to(n): return n if n == 0 else n + sum_to(n - 1)` викликає RecursionError приблизно на `sum_to(2000)`, але ітеративний цикл чи `sum(range(n + 1))` обробляє будь-яке велике `n` зі сталим використанням пам'яті.

#recursion#performance

Джерела

The Python Language ReferencePython Software Foundation · Precise scoping, evaluation order and expression semanticsThe Python Standard LibraryPython Software Foundation · Authoritative behavior for built-in types and standard library modules
Functions and functional toolsJuniorCommonPractical

Як `*` і `**` розпаковують колекцію у виклик функції, і чи можна їх поєднувати?

Відповідь

У місці виклику `*iterable` розпаковує кожен елемент списку, кортежу чи іншого ітерованого об'єкта в окремий позиційний аргумент, а `**mapping` розпаковує кожну пару ключ-значення словника в окремий іменований аргумент, зіставлений за іменем параметра. Обидва можна поєднувати в одному виклику, і навіть кілька зіркових аргументів можна об'єднувати з різних джерел, доки результуючий набір позиційних та іменованих аргументів справді відповідає тому, що приймає цільова функція.

Сильна відповідь включає

  • * розпаковує ітерований об'єкт у позиційні аргументи
  • ** розпаковує словник у іменовані аргументи за іменем
  • обидва можна поєднувати й об'єднувати з кількох джерел

Практичний приклад

`args = (2, 3); kwargs = {"base": 10}; pow(*args, **kwargs)` викликає функцію так, ніби написано `pow(2, 3, base=10)`, розгортаючи кортеж як позиційні аргументи, а словник — як іменовані.

#functions#unpacking

Джерела

The Python TutorialPython Software Foundation · Baseline syntax, control flow, functions and module basicsThe Python Language ReferencePython Software Foundation · Precise scoping, evaluation order and expression semantics
Functions and functional toolsMiddleCommonTheory

Чи забезпечують анотації типів функцій Python примусову перевірку типів під час виконання?

Відповідь

Ні. Анотації на кшталт `def add(a: int, b: int) -> int:` мають суто інформативний характер для інтерпретатора — Python ніколи не перевіряє, не приводить і не відхиляє аргументи на їхній основі під час виклику, тож `add("2", "3")` виконується без помилки й повертає `"23"`. З Python 3.14 (PEP 649/749) анотації обчислюються ліниво за замовчуванням, а не обчислюються й зберігаються одразу в момент визначення функції — базові вирази обчислюються за потребою, через `__annotations__` чи модуль `annotationlib`, що уникає проблем із випереджувальними посиланнями й знижує накладні витрати на імпорт порівняно з попередніми версіями. Примусова перевірка типів відбувається, лише якщо окремий статичний перевіряч типів, як-от mypy чи pyright, аналізує код заздалегідь, якщо сама функція явно валідує свої вхідні дані під час виконання, чи якщо якийсь інший споживач під час виконання — наприклад, вебфреймворк, що валідує вхідні дані запиту — сам вирішує прочитати й використати анотації.

Сильна відповідь включає

  • анотації суто інформативні, ніколи не примушуються й не приводяться при виклику
  • Python 3.14 обчислює анотації ліниво за замовчуванням (PEP 649/749), не одразу при визначенні
  • перевірка вимагає окремого статичного інструменту, явної валідації чи споживача, що сам інспектує анотації

Практичний приклад

`def add(a: int, b: int) -> int: return a + b` спокійно виконує `add("2", "3")` і повертає зчеплений рядок `"23"`, бо `+` для рядків коректний, а анотації жодного разу не перевірялися.

#typing#gotchas

Джерела

typing — Support for type hintsPython Software Foundation · Generics, protocols, overloads and typing-module semanticsPEP 484 — Type HintsPython Software Foundation · Foundational type-hint syntax and static-typing intent
Object-oriented PythonJuniorVery commonTheory

Що таке `self`, і чому його потрібно явно вказувати першим параметром кожного методу екземпляра?

Відповідь

`self` — це просто загальноприйняте ім'я параметра, що отримує екземпляр, на якому викликається метод; Python не використовує неявний `this`, як деякі інші мови. Коли ви пишете `obj.method(arg)`, Python під капотом перетворює це на `ClassName.method(obj, arg)`, тож екземпляр завжди передається першим позиційним аргументом, і сигнатура методу повинна явно його приймати. `self` — угода іменування, а не ключове слово; спрацювало б будь-яке ім'я, але кожен посібник зі стилю Python очікує саме `self`.

Сильна відповідь включає

  • self отримує екземпляр, на якому викликається метод
  • obj.method(arg) перетворюється на Class.method(obj, arg)
  • self — угода, а не зарезервоване слово

Практичний приклад

`instance.greet()` і `Person.greet(instance)` викликають ту саму функцію з тими самими аргументами; `self` усередині `greet` в обох випадках посилається на `instance`.

#oop#classes

Джерела

The Python TutorialPython Software Foundation · Baseline syntax, control flow, functions and module basicsData modelPython Software Foundation · Dunder methods, object identity, attribute lookup and the descriptor and context manager protocolsTop Python Interview Questions and AnswersGeeksforGeeks · Coverage signal for frequently asked fundamentals and OOP questions
Object-oriented PythonMiddleCommonTheory

Що таке порядок розв'язання методів (MRO), і як його використовує `super()`?

Відповідь

MRO — це лінійний порядок, у якому Python шукає атрибут чи метод у класі та його предках, обчислений алгоритмом C3-лінеаризації так, що підклас завжди йде перед своїми батьками і зберігається послідовний порядок серед усіх базових класів. Його можна переглянути через `ClassName.__mro__`. `super()` не означає просто «мій прямий батьківський клас»; він повертає проксі, що продовжує пошук з наступного класу в MRO *поточного екземпляра* після класу, що викликає, — саме це робить кооперативну множинну спадковість коректною.

Сильна відповідь включає

  • MRO обчислюється C3-лінеаризацією, доступний через __mro__
  • підклас завжди йде перед батьками в порядку
  • super() продовжує з наступного класу в MRO екземпляра, а не буквально батька

Практичний приклад

У ромбі `class D(B, C):`, де і `B`, і `C` успадковують від `A`, `D.__mro__` дорівнює `(D, B, C, A, object)`, і виклик `super().__init__()` всередині `B` переходить до `C`, а не напряму до `A`.

#inheritance#mro#super

Джерела

Data modelPython Software Foundation · Dunder methods, object identity, attribute lookup and the descriptor and context manager protocolsThe Python Language ReferencePython Software Foundation · Precise scoping, evaluation order and expression semantics
Object-oriented PythonMiddleVery commonTheory

Яка різниця між `__repr__` і `__str__`, і що кожен з них має повертати?

Відповідь

`__repr__` має повертати однозначне, орієнтоване на розробника представлення, в ідеалі таке, яке можна було б обчислити, щоб відтворити еквівалентний об'єкт (наприклад, `Point(x=1, y=2)`), і саме воно з'являється в REPL, у надрукованому вмісті списку та в дебагері. `__str__` має повертати читабельний, орієнтований на користувача опис, призначений для викликів `print()` та `str()`. Якщо `__str__` не визначено, Python автоматично повертається до `__repr__`, тож хорошою практикою є завжди реалізовувати `__repr__`, навіть якщо `__str__` пропущено.

Сильна відповідь включає

  • __repr__ однозначний, орієнтований на розробника, бажано відтворюваний
  • __str__ читабельний, орієнтований на користувача, для print()
  • Python повертається до __repr__, якщо __str__ відсутній

Практичний приклад

`repr(datetime(2026, 1, 1))` повертає `"datetime.datetime(2026, 1, 1, 0, 0)"`, точне й відтворюване, тоді як `str(datetime(2026, 1, 1))` повертає приязніше `"2026-01-01 00:00:00"`.

#dunder-methods#oop

Джерела

Data modelPython Software Foundation · Dunder methods, object identity, attribute lookup and the descriptor and context manager protocols
Object-oriented PythonMiddleVery commonTheory

Яка різниця між методом екземпляра, `@classmethod` і `@staticmethod`?

Відповідь

Метод екземпляра автоматично отримує екземпляр як перший аргумент (`self`) і може читати чи змінювати стан цього екземпляра. `@classmethod` автоматично отримує сам клас як перший аргумент (`cls`) замість екземпляра, що корисно для альтернативних конструкторів, яким потрібно знати точний клас, включно з підкласом, без жорсткого зашивання імені класу. `@staticmethod` не отримує ні `self`, ні `cls`; він поводиться як звичайна функція, що просто розміщена всередині класу для організації, без автоматичного доступу до стану екземпляра чи класу.

Сильна відповідь включає

  • метод екземпляра отримує self і може змінювати стан
  • classmethod отримує cls, часто для альтернативних конструкторів
  • staticmethod не отримує жодного, просто розміщений у класі

Практичний приклад

`@classmethod def from_json(cls, data): return cls(**json.loads(data))` коректно будує потрібний підклас, якщо викликано як `Subclass.from_json(...)`, бо `cls` — це саме той клас, на якому реально викликано метод.

#oop#classmethod#staticmethod

Джерела

The Python Language ReferencePython Software Foundation · Precise scoping, evaluation order and expression semanticsData modelPython Software Foundation · Dunder methods, object identity, attribute lookup and the descriptor and context manager protocols
Object-oriented PythonMiddleCommonPractical

Яку проблему вирішує декоратор `@property`, і як він дозволяє додати валідацію, не ламаючи наявний доступ у стилі атрибута?

Відповідь

`@property` перетворює метод на щось, доступне як звичайний атрибут (`obj.value` замість `obj.value()`), що дозволяє класу почати з простих публічних атрибутів, а пізніше додати обчислювану поведінку, валідацію чи логування, не змушуючи кожного викликача переписувати `obj.value` на `obj.get_value()`. Парний метод `@value.setter` може перевіряти чи перетворювати присвоєне значення перед внутрішнім збереженням, тож `obj.value = -5` може викликати `ValueError` зсередини того, що для викликача все ще виглядає як звичайне присвоєння атрибуту.

Сильна відповідь включає

  • надає доступ до методу через синтаксис атрибута
  • дозволяє додати валідацію пізніше без злому публічного API
  • парний @x.setter перехоплює присвоєння для валідації

Практичний приклад

`@property\n def radius(self): return self._radius\n@radius.setter\ndef radius(self, value):\n if value < 0: raise ValueError("radius must be non-negative")\n self._radius = value` дозволяє `circle.radius = 5` і відхиляє `circle.radius = -1`.

#property#encapsulation#oop

Джерела

Data modelPython Software Foundation · Dunder methods, object identity, attribute lookup and the descriptor and context manager protocolsThe Python Language ReferencePython Software Foundation · Precise scoping, evaluation order and expression semantics
Object-oriented PythonMiddleCommonPractical

Що генерує декоратор `@dataclass`, і коли звичайний клас все ще кращий вибір?

Відповідь

`@dataclass` аналізує анотовані поля класу й автоматично генерує `__init__`, `__repr__` та `__eq__` (що порівнює всі поля), усуваючи шаблонний код, який довелося б писати вручну для класу, що переважно просто зберігає дані. `frozen=True` робить так, що присвоєння полю після `__init__` викликає помилку — `instance.x = 5` провалюється, — що емулює поля лише для читання, але не робить екземпляр глибоко незмінюваним: поле зі змінюваним об'єктом на кшталт списку все ще можна змінити через власні методи цього об'єкта. Хешованість — окрема, пов'язана опція: dataclass за замовчуванням нехешований, щойно `eq=True` (типове значення) генерує `__eq__`, бо змінюваний об'єкт з рівністю за значенням навмисно робиться нехешованим, щоб уникнути тихого пошкодження множини чи словника; саме `frozen=True` разом з `eq=True` робить екземпляри хешованими, і лише якщо поточне значення кожного поля саме хешоване. Звичайний клас все ще кращий вибір, коли тип має значну власну поведінку, інваріанти, що застосовуються в багатьох методах, або ієрархію спадкування, де згенеровані dunder-методи не робили б правильну річ.

Сильна відповідь включає

  • frozen=True забороняє повторне присвоєння полю, але не робить незмінюваними пов'язані змінювані об'єкти
  • хешованість залежить від frozen=True разом з eq=True та від хешованості кожного поля
  • звичайний клас кращий для складної поведінки чи ієрархій

Практичний приклад

`@dataclass(frozen=True)\nclass Point:\n x: float\n y: float` дає повністю робочий, хешований, незмінюваний тип значення у чотирьох рядках замість приблизно двадцяти рядків вручну написаних dunder-методів.

#dataclasses#oop

Джерела

dataclasses — Data ClassesPython Software Foundation · Generated dunder methods, field defaults and frozen/eq/order semantics
Object-oriented PythonSeniorCommonDesign

Чому досвідчені розробники Python часто надають перевагу композиції над глибокими ієрархіями спадкування?

Відповідь

Глибоке спадкування тісно прив'язує підклас до деталей реалізації предків, тож зміна на кілька рівнів вище може тихо зламати поведінку далеко внизу ієрархії — відома проблема «крихкого базового класу», і воно нав'язує відношення «є» (is-a) навіть тоді, коли реальне відношення ближче до «має» (has-a). Композиція будує клас з менших, незалежно тестованих об'єктів-співробітників, переданих ззовні чи створених внутрішньо, що тримає кожну частину фокусованою, робить залежності явними й замінними (корисно для тестування з фейками) і уникає прив'язки до єдиної фіксованої форми ієрархії, вирішеної рано в проєкті.

Сильна відповідь включає

  • глибоке спадкування створює проблему крихкого базового класу
  • композиція тримає співробітників незалежно тестованими й замінними
  • уникає нав'язування is-a там, де підходить has-a

Практичний приклад

`ReportGenerator`, що приймає об'єкти `formatter` і `data_source` у конструкторі, можна тестувати, підставляючи фейки для обох, а перехід на новий формат виводу ніколи не вимагає зміни власної ієрархії класів `ReportGenerator`.

#design#composition#inheritance

Джерела

Data modelPython Software Foundation · Dunder methods, object identity, attribute lookup and the descriptor and context manager protocols
Object-oriented PythonSeniorOccasionalDesign

Як абстрактні базові класи (`abc.ABC`) і `typing.Protocol` обидва виражають інтерфейс, але по-різному?

Відповідь

Підклас `abc.ABC` з методами, позначеними `@abstractmethod`, забезпечує номінальну типізацію: клас повинен явно успадковуватися від ABC, і Python видасть `TypeError` під час створення екземпляра, якщо якийсь абстрактний метод не реалізовано. `typing.Protocol` натомість виражає структурну типізацію: клас задовольняє протокол просто маючи потрібні методи й атрибути, без вимоги успадкування, а порушення виявляє статичний перевіряч типів, а не час виконання. ABC пасують для явної, курованої ієрархії; протоколи пасують для перевірки довільних об'єктів з качиною типізацією, включно з тими, що з бібліотек, які ви не контролюєте.

Сильна відповідь включає

  • ABC забезпечує номінальну типізацію, TypeError при створенні
  • Protocol виражає структурну типізацію без вимоги спадкування
  • порушення протоколу виявляються статично, не під час виконання

Практичний приклад

`class Sized(Protocol): def __len__(self) -> int: ...` задовольняється будь-яким класом, що визначає `__len__`, включно з вбудованими `list` і `dict`, хоча жоден з них ніколи не успадковується від `Sized`.

#typing#protocols#abc

Джерела

typing — Support for type hintsPython Software Foundation · Generics, protocols, overloads and typing-module semanticsPEP 484 — Type HintsPython Software Foundation · Foundational type-hint syntax and static-typing intent
Object-oriented PythonSeniorOccasionalTroubleshooting

Як Python розв'язує класичну проблему ромбоподібного спадкування, і що на практиці означає «кооперативна множинна спадковість»?

Відповідь

У ромбі, де і `B`, і `C` успадковують від `A`, а `D` успадковує від обох `B` і `C`, C3-лінеаризований MRO Python гарантує, що кожен предок з'являється рівно один раз у послідовному порядку, тож немає неоднозначності щодо того, який саме `A` використовується. Кооперативна множинна спадковість означає, що кожен клас в ієрархії викликає `super().__init__(**kwargs)` (замість виклику конкретного батьківського класу за іменем), щоб кожен `__init__` у ланцюжку виконався рівно один раз, у порядку MRO, передаючи далі ті іменовані аргументи, які ще потрібні наступному класу в черзі.

Сильна відповідь включає

  • C3-лінеаризований MRO гарантує кожного предка рівно один раз
  • кооперативний патерн використовує super() замість конкретного батька за іменем
  • кожен __init__ виконується один раз у порядку MRO через ланцюжок super()

Практичний приклад

Якщо `B.__init__` і `C.__init__` обидва викликають `A.__init__` напряму замість `super().__init__()`, `A.__init__` виконається двічі в екземплярі `D()`; використання `super().__init__()` скрізь робить так, що він виконується рівно один раз.

#inheritance#mro#super

Джерела

Data modelPython Software Foundation · Dunder methods, object identity, attribute lookup and the descriptor and context manager protocolsThe Python Language ReferencePython Software Foundation · Precise scoping, evaluation order and expression semantics
Object-oriented PythonSeniorOccasionalPerformance

Що робить визначення `__slots__` у класі, і який компроміс воно вносить?

Відповідь

За замовчуванням кожен екземпляр отримує власний `__dict__` для зберігання довільних атрибутів, що гнучко, але має реальні накладні витрати пам'яті на екземпляр. Визначення `__slots__ = ("x", "y")` каже Python виділити структуру фіксованого розміру саме для цих названих атрибутів замість словника на екземпляр, що суттєво знижує використання пам'яті при створенні дуже великої кількості екземплярів невеликого класу. Компроміс — екземпляри класу зі слотами більше не можуть отримувати довільні нові атрибути під час виконання, — але лише якщо ніщо в ієрархії класу все ще не надає `__dict__`: якщо базовий клас узагалі не визначає `__slots__`, чи власні `__slots__` підкласу включають `"__dict__"`, екземпляри все одно його отримують, і перевага з пам'яттю для цієї ієрархії втрачається. Множинна спадковість зі слотами з кількох непорожніх базових класів також обмежена.

Сильна відповідь включає

  • без слотів кожен екземпляр несе гнучкий словник
  • слоти виділяють структуру фіксованого розміру, зменшуючи пам'ять
  • заборона нових атрибутів діє лише якщо ніщо в ієрархії не надає __dict__

Практичний приклад

Клас, що тримає мільйони маленьких об'єктів-координат, може суттєво скоротити споживання пам'яті, додавши `__slots__ = ("x", "y")`, ціною того, що `point.label = "origin"` тепер викличе AttributeError.

#performance#memory#slots

Джерела

Data modelPython Software Foundation · Dunder methods, object identity, attribute lookup and the descriptor and context manager protocols
Object-oriented PythonMiddleCommonTheory

Яка практична різниця між одним і двома провідними підкресленнями в назві атрибута?

Відповідь

Одне провідне підкреслення, як `_value`, — це суто угода, що означає «внутрішнє, будь ласка, не чіпайте ззовні класу», без жодного примусу з боку інтерпретатора. Подвійне провідне підкреслення (без кінцевого подвійного підкреслення), як `__value`, запускає перейменування імені (name mangling): Python внутрішньо переписує ім'я на `_ClassName__value`, що покликане уникнути випадкових конфліктів атрибутів у підкласах, а не забезпечити справжню приватність, бо перейменоване ім'я все ще повністю доступне, якщо знати патерн.

Сильна відповідь включає

  • одне підкреслення — лише угода, без примусу
  • подвійне підкреслення запускає перейменування на _ClassName__value
  • перейменування уникає конфліктів у підкласах, не є справжньою приватністю

Практичний приклад

У `class Account: def __init__(self): self.__balance = 0` атрибут насправді зберігається як `_Account__balance`, тож `account._Account__balance` все ще читає його ззовні класу.

#encapsulation#naming-conventions

Джерела

The Python Language ReferencePython Software Foundation · Precise scoping, evaluation order and expression semanticsPEP 8 — Style Guide for Python CodePython Software Foundation · Naming, layout and readability conventions
Iterators, generators and decoratorsMiddleVery commonTheory

Що таке протокол ітератора, і як він пов'язаний з різницею між iterable (ітерованим) і iterator (ітератором)?

Відповідь

Iterable — це будь-який об'єкт, для якого `iter(obj)` може отримати ітератор — найчастіше через реалізацію `__iter__`, що повертає ітератор, але протокол ітерації Python також визнає старіший протокол послідовностей: об'єкт без `__iter__`, але з робочим `__getitem__(self, index)`, що починається з індексу 0, все ще iterable, бо `iter()` переходить до виклику його зі зростаючими цілими індексами, доки не отримає `IndexError`. Ітератор — це будь-який об'єкт, що реалізує і `__iter__` (зазвичай повертаючи себе), і `__next__`, який повертає наступне значення або викликає `StopIteration`, коли вичерпано. Цикл `for` по суті — це синтаксичний цукор для одноразового виклику `iter(obj)`, щоб отримати ітератор, а потім повторного виклику `next()` на ньому, доки не буде перехоплено `StopIteration`. Список — це iterable, але сам по собі не ітератор: `iter([1, 2, 3])` щоразу створює новий, окремий об'єкт-ітератор списку.

Сильна відповідь включає

  • iterable — усе, для чого iter() може отримати ітератор — зазвичай __iter__, або застарілий протокол __getitem__
  • ітератор реалізує __iter__ і __next__, викликає StopIteration при вичерпанні
  • цикл for — це цукор для iter(), а потім повторного next()

Практичні приклади

Що цикл for робить всередині
values = [10, 20]
it = iter(values)

while True:
    try:
        value = next(it)
    except StopIteration:
        break
    else:
        print(value)

for один раз отримує iterator через iter(), повторно викликає next() і завершується на StopIteration. List є iterable; окремий об'єкт від iter(list) є iterator.

Очікуваний результат: 10, потім 20.

#iterators#protocols

Джерела

Data modelPython Software Foundation · Dunder methods, object identity, attribute lookup and the descriptor and context manager protocolsThe Python TutorialPython Software Foundation · Baseline syntax, control flow, functions and module basics
Iterators, generators and decoratorsMiddleVery commonTheory

Чим функція з `yield` поводиться інакше, ніж звичайна функція?

Відповідь

Будь-яка функція, тіло якої містить хоча б один оператор `yield`, стає функцією-генератором: її виклик взагалі не виконує тіло, а одразу повертає об'єкт-генератор, що автоматично реалізує протокол ітератора. Кожен виклик `next()` на цьому генераторі виконує тіло функції до наступного виразу `yield`, зупиняє виконання там і повертає видане значення; увесь локальний стан функції — змінні, вказівник інструкцій, навіть відкриті цикли — зберігається між викликами, і виконання відновлюється точно з того місця, де зупинилося, на наступному `next()`.

Сильна відповідь включає

  • наявність yield робить функцію генератором
  • виклик повертає об'єкт-генератор без виконання тіла
  • виконання зупиняється на yield і відновлюється зі збереженим локальним станом

Практичні приклади

Generator зупиняється і продовжується навколо yield
def countdown(n):
    print("started")
    while n > 0:
        yield n
        n -= 1

gen = countdown(3)
print("created")
print(next(gen))
print(next(gen))
print(list(gen))

Виклик countdown створює generator без виконання тіла. Кожне споживання продовжує виконання до наступного yield і зберігає n між відновленнями.

Очікуваний результат: created, started, 3, 2, потім [1].

#generators#yield

Джерела

The Python Language ReferencePython Software Foundation · Precise scoping, evaluation order and expression semanticsThe Python TutorialPython Software Foundation · Baseline syntax, control flow, functions and module basicsReal Python tutorialsReal Python · Coverage and explanation-style cross-check for intermediate and advanced Python topics
Iterators, generators and decoratorsSeniorSpecialistTheory

Чим `generator.send(value)` відрізняється від `next(generator)`, і що це дозволяє реалізувати?

Відповідь

`next(gen)` відновлює генератор і неявно передає `None` у призупинений вираз `yield`. `gen.send(value)` відновлює генератор і робить це `value` результатом самого призупиненого виразу `yield`, перетворюючи генератор на двосторонній канал спілкування, а не одностороннього виробника значень. Саме цей механізм зробив можливими генератори у стилі корутин до появи нативних `async`/`await`, хоча сучасний код майже завжди надає перевагу `async`/`await` для цього випадку, залишаючи звичайні генератори для простого виробництва значень.

Сильна відповідь включає

  • next() неявно передає None у призупинений yield
  • send(value) робить value результатом виразу yield
  • історично уможливило генератори-корутини, тепер витіснено async/await

Практичний приклад

`def echo():\n while True:\n received = yield\n print("got", received)` потребує одноразового «прайму» через `next(gen)`, після чого `gen.send("hi")` друкує `got hi` і знову зупиняється на тому самому `yield`.

#generators#coroutines

Джерела

The Python Language ReferencePython Software Foundation · Precise scoping, evaluation order and expression semanticsPEP 380 — Syntax for Delegating to a SubgeneratorPython Software Foundation · yield from delegation semantics
Iterators, generators and decoratorsSeniorOccasionalTheory

Що робить `yield from`, і яку проблему воно вирішило порівняно з ручним циклом і повторним yield?

Відповідь

`yield from subgenerator` делегує ітерацію підгенератору `subgenerator`, прозоро видаючи кожне значення, яке він виробляє, так, ніби зовнішній генератор видав їх напряму, а також передає виклики `send()`, `throw()` і `close()` вниз до підгенератора й повертає його значення `return` як значення самого виразу `yield from`. До того, як PEP 380 ввів це в Python 3.3, делегування підгенератору вимагало ручного циклу `for item in subgenerator: yield item`, який втрачає можливість коректно передавати надіслані значення, кинуті винятки й значення `return` підгенератора.

Сильна відповідь включає

  • делегує ітерацію, send, throw і close підгенератору
  • вираз yield from обчислюється до значення return підгенератора
  • замінює схильний до помилок ручний цикл for з yield

Практичний приклад

`def all_lines(files):\n for path in files:\n yield from open(path)` послідовно передає кожен рядок з кожного файлу без ручного повторного yield.

#generators#yield-from

Джерела

PEP 380 — Syntax for Delegating to a SubgeneratorPython Software Foundation · yield from delegation semanticsThe Python Language ReferencePython Software Foundation · Precise scoping, evaluation order and expression semantics
Iterators, generators and decoratorsMiddleCommonTroubleshooting

Чому повторна ітерація генератора не дає жодних елементів, і як це обійти?

Відповідь

Генератор тримає стан виконання внутрішньо і вичерпується в момент, коли викликає `StopIteration`; вбудованого способу «перемотати» його немає, тож другий цикл `for` чи виклик `list()` над тим самим об'єктом-генератором просто нічого не дасть. На відміну від списку, генератор не багаторазового використання. Обхідний шлях — викликати функцію-генератор знову, щоб отримати новий об'єкт-генератор, або матеріалізувати значення у список один раз, якщо дійсно потрібно ітерувати їх більше одного разу, приймаючи витрати пам'яті на це.

Сильна відповідь включає

  • генератор вичерпується після одного StopIteration, без перемотки
  • другий прохід над тим самим генератором нічого не дає
  • обхід: викликати функцію-генератор знову або матеріалізувати у список

Практичні приклади

Generator одноразовий
def numbers():
    yield 1
    yield 2

gen = numbers()
print(list(gen))
print(list(gen))

# Create a new generator when another pass is needed
print(list(numbers()))

Після досягнення StopIteration generator залишається вичерпаним. Викличте generator-функцію знову, щоб отримати новий generator для повторного проходу.

Очікуваний результат: [1, 2], потім [], потім [1, 2].

#generators#gotchas

Джерела

The Python Language ReferencePython Software Foundation · Precise scoping, evaluation order and expression semantics
Iterators, generators and decoratorsMiddleVery commonTheory

Що таке декоратор, і що насправді робить `@my_decorator` над визначенням функції?

Відповідь

Декоратор — це виклична сутність, що приймає ціль — найчастіше функцію чи клас — і повертає її заміну, зазвичай обгортку, що додає поведінку навколо оригіналу. Фабрика декораторів, як `retry(times=3)`, — пов'язана, але окрема ідея: це окрема виклична сутність, що сама повертає справжній декоратор, і саме це дозволяє працювати `@retry(times=3)`, хоча декоратор, застосований через `@`, концептуально завжди приймає лише одну ціль. Написання `@my_decorator` прямо над `def func(): ...` точно еквівалентне звичайному визначенню `func`, а потім негайному повторному прив'язуванню імені через `func = my_decorator(func)`; синтаксис декоратора — це суто скорочення для читабельності цього повторного прив'язування, що обчислюється щоразу, коли виконується сам охоплюючий оператор `def` — при імпорті модуля для функції верхнього рівня, але під час виклику для функції, визначеної всередині іншої функції, а не обов'язково «коли модуль завантажується».

Сильна відповідь включає

  • декоратор приймає ціль (функцію чи клас) і повертає її заміну
  • фабрика декораторів — окрема сутність, що сама повертає справжній декоратор, напр. для @retry(times=3)
  • @decorator дорівнює name = decorator(name), обчислюється при виконанні цього def — не завжди при завантаженні модуля

Практичний приклад

`@timed\ndef slow_task(): ...` — це точно те саме, що `def slow_task(): ...`, а потім `slow_task = timed(slow_task)`; потрібен лише один із цих двох записів, ніколи обидва.

#decorators

Джерела

The Python Language ReferencePython Software Foundation · Precise scoping, evaluation order and expression semanticsTop Python Interview Questions and AnswersInterviewBit · Coverage signal across syntax, decorators, generators and modules
Iterators, generators and decoratorsSeniorCommonPractical

Як написати декоратор, що сам приймає аргументи, наприклад `@retry(times=3)`?

Відповідь

Декоратор, що приймає аргументи, потребує одного додаткового рівня вкладеності: найзовнішня функція приймає власні аргументи декоратора й повертає справжню функцію-декоратор, яка приймає цільову функцію й повертає обгортку, яка вже приймає аргументи цільової функції під час виклику. Тож `@retry(times=3)` спершу викликає `retry(times=3)`, яка повертає справжній декоратор, і цей декоратор потім застосовується до функції одразу під ним, точно так само, як звичайний декоратор без аргументів.

Сильна відповідь включає

  • потрібен додатковий зовнішній рівень, що повертає справжній декоратор
  • зовнішній рівень приймає власні аргументи декоратора
  • повернутий декоратор застосовується до функції як зазвичай

Практичний приклад

`def retry(times):\n def decorator(func):\n def wrapper(*args, **kwargs):\n for _ in range(times):\n try:\n return func(*args, **kwargs)\n except Exception:\n continue\n raise\n return wrapper\n return decorator` визначає трирівневу фабрику декораторів.

#decorators#advanced

Джерела

The Python Language ReferencePython Software Foundation · Precise scoping, evaluation order and expression semantics
Iterators, generators and decoratorsSeniorCommonPractical

Чому добре написаний декоратор використовує `functools.wraps`, і що ламається, якщо це пропустити?

Відповідь

Без `functools.wraps` функція-обгортка декоратора повністю замінює оригінальну функцію, включно з її `__name__`, `__doc__`, `__module__` і метаданими `__wrapped__`, тож `help(decorated_func)`, інструменти інтроспекції та все, що покладається на `func.__name__` (як деякі тестові фреймворки й дебагери), бачать загальну обгортку замість реальної функції. `@functools.wraps(func)`, застосований до обгортки, автоматично копіює ці метадані, а також встановлює `__wrapped__` на оригінальну функцію, зберігаючи ілюзію, що декорована функція все ще впізнавана як вона сама.

Сильна відповідь включає

  • без wraps __name__/__doc__/__module__ замінюються на дані обгортки
  • ламає інструменти інтроспекції й усе, що читає func.__name__
  • functools.wraps копіює метадані й встановлює __wrapped__

Практичний приклад

Без `@functools.wraps(func)` усередині декоратора `@log_calls` `decorated_greet.__name__` показує `"wrapper"` замість `"greet"`, що ламає автоматично згенеровану API-документацію, яка виводить імена функцій.

#decorators#functools

Джерела

functools — Higher-order functions and operations on callable objectsPython Software Foundation · lru_cache, partial, wraps, reduce and singledispatch behavior
Iterators, generators and decoratorsSeniorOccasionalPractical

Як можна використати клас як декоратор замість вкладеної функції, і коли такий підхід кращий?

Відповідь

Клас стає придатним для використання як декоратор, коли реалізує `__init__` для отримання декорованої функції та `__call__` для отримання аргументів виклику й виклику збереженої функції, оскільки `@MyDecorator`, застосований до функції, — це просто `func = MyDecorator(func)`, а подальший виклик `func(...)` стає `instance.__call__(...)`. Декоратор на основі класу кращий, коли декоратору потрібно тримати й керувати значущим внутрішнім станом між викликами (як лічильник викликів чи кеш), бо такий стан природно живе як атрибути екземпляра, а не незграбно вкладені змінні замикання.

Сильна відповідь включає

  • __init__ отримує функцію, __call__ обробляє виклик
  • @MyDecorator на функції дорівнює func = MyDecorator(func)
  • кращий, коли декоратору потрібен реальний внутрішній стан

Практичний приклад

`class CountCalls:\n def __init__(self, func):\n self.func = func\n self.calls = 0\n def __call__(self, *a, **kw):\n self.calls += 1\n return self.func(*a, **kw)` напряму надає `decorated_func.calls` як стан екземпляра.

#decorators#oop

Джерела

Data modelPython Software Foundation · Dunder methods, object identity, attribute lookup and the descriptor and context manager protocols
Iterators, generators and decoratorsMiddleOccasionalPractical

Як `itertools.chain` та `itertools.islice` допомагають компонувати ітератори, не втрачаючи лінивості?

Відповідь

`itertools.chain(*iterables)` виробляє єдиний лінивий ітератор, що проходить по черзі через кожен вхідний iterable, не з'єднуючи їх спершу в один матеріалізований набір, тож працює навіть коли вхідні дані самі є нескінченними генераторами. `itertools.islice(iterable, stop)` (або з start/step) ліниво бере обмежений зріз з будь-якого ітератора, включно з тим, що не має визначеної довжини, що дозволяє безпечно взяти обмежену кількість елементів з інакше необмеженого генератора, ніколи не викликаючи `list()` на всьому наборі.

Сильна відповідь включає

  • chain ліниво проходить кілька iterable, працює з нескінченними генераторами
  • islice бере обмежений лінивий зріз з будь-якого ітератора
  • жоден не матеріалізує базову послідовність

Практичний приклад

`list(itertools.islice(itertools.count(1), 5))` безпечно повертає `[1, 2, 3, 4, 5]`, навіть попри те, що `itertools.count(1)` — нескінченний генератор, який сам по собі ніколи не завершується.

#itertools#generators

Джерела

itertools — Functions creating iterators for efficient loopingPython Software Foundation · Lazy iterator composition and memory-efficient looping patterns
Iterators, generators and decoratorsMiddleCommonPractical

Коли над однією функцією накладено декілька декораторів, у якому порядку вони застосовуються?

Відповідь

Декоратори застосовуються знизу вгору, але виконуються ззовні всередину. З `@a`, потім `@b`, потім `def f():` Python спершу застосовує `b` до `f`, а потім застосовує `a` до результату цього, що еквівалентно `f = a(b(f))`. Але під час виклику виконання йде у зворотному напрямку: виклик фінальної декорованої `f()` спершу входить в обгортку `a`, яка зазвичай викликає обгортку `b`, яка вже врешті викликає оригінальну `f`. Плутанина в цьому — поширене джерело помилок, коли порядок декораторів реально важливий, наприклад застосування `@app.route(...)` над `@login_required` чи навпаки.

Сильна відповідь включає

  • застосовуються знизу вгору: найближчий декоратор обгортає першим
  • еквівалентно f = a(b(f)) для стосу @a @b
  • виконання під час виклику йде ззовні всередину через обгортки

Практичний приклад

`@log\n@cache\ndef compute(): ...` спершу застосовує `cache`, потім `log`, тож під час виклику обгортка `log` виконується першою і може виміряти час усього кешованого виклику, включно з промахом кешу, що проходить до `compute`.

#decorators#gotchas

Джерела

The Python Language ReferencePython Software Foundation · Precise scoping, evaluation order and expression semantics
Errors and context managersMiddleCommonTheory

Де `Exception` розташований в ієрархії винятків Python відносно `BaseException`, і чому ця різниця важлива для голого `except:`?

Відповідь

`BaseException` — корінь усієї ієрархії, а `Exception` — один з його прямих підкласів, що охоплює по суті всі помилкові стани, які код застосунку має обробляти. Критично, `SystemExit`, `KeyboardInterrupt` і `GeneratorExit` успадковуються напряму від `BaseException`, а не від `Exception`, саме для того, щоб звичайний `except Exception:` випадково не поглинув користувацький Ctrl+C чи навмисний виклик `sys.exit()`. Голий `except:` перехоплює все, включно з цими трьома, тому він майже завжди гірший вибір за `except Exception:`.

Сильна відповідь включає

  • BaseException — корінь, Exception — підклас для звичайних помилок
  • SystemExit/KeyboardInterrupt/GeneratorExit успадковують напряму BaseException
  • голий except: перехоплює й ці три, на відміну від except Exception:

Практичний приклад

Голий `except:` усередині циклу повторних спроб може випадково перехопити й приховати `KeyboardInterrupt`, через що програма виглядає завислою і повністю ігнорує Ctrl+C.

#exceptions#error-handling

Джерела

Errors and ExceptionsPython Software Foundation · try/except/else/finally control flow and exception chainingThe Python Language ReferencePython Software Foundation · Precise scoping, evaluation order and expression semantics
Errors and context managersJuniorVery commonPractical

Який порядок виконання `try`, `except`, `else` і `finally`, і для чого саме служить блок `else`?

Відповідь

Python спочатку виконує блок `try`; якщо виникає виняток, керування переходить до першого відповідного блоку `except`, а якщо винятку немає — одразу після успішного завершення `try` виконується необов'язковий блок `else`. `finally` завжди виконується останнім, незалежно від того, чи виник виняток і чи був він оброблений, що робить його правильним місцем для гарантованого очищення. Блок `else` існує, щоб відокремити «код, що може викликати виняток, який ми ловимо» від «коду, що має виконатися лише якщо винятку не було», уникаючи випадкового перехоплення винятків, викликаних цим наступним кодом усередині самого блоку `try`.

Сильна відповідь включає

  • try виконується, потім except при помилці чи else при успіху, finally завжди останній
  • finally — гарантоване очищення незалежно від результату
  • else відокремлює захищений код від наступного коду, який не варто перехоплювати

Практичні приклади

Винесіть наступну роботу за межі захищеного try
def parse_and_use(text):
    try:
        value = int(text)
    except ValueError:
        print("invalid")
    else:
        print(100 // value)
    finally:
        print("cleanup")

parse_and_use("4")
parse_and_use("bad")

except обробляє лише помилки захищеного parse. else виконується лише після успішного try, а finally — в обох шляхах і підходить для гарантованого cleanup.

Очікуваний результат: Для '4': 25, потім cleanup. Для 'bad': invalid, потім cleanup.

#exceptions#control-flow

Джерела

Errors and ExceptionsPython Software Foundation · try/except/else/finally control flow and exception chaining
Errors and context managersMiddleCommonPractical

Чому застосункам варто визначати власні класи винятків, і що додає `raise NewError(...) from original_error`?

Відповідь

Власні класи винятків дозволяють коду, що викликає, перехоплювати й реагувати на конкретні доменні помилки (`InsufficientFundsError`) замість загальних (`ValueError`), і вони можуть нести структуровані дані про помилку як атрибути, а не лише відформатований рядок повідомлення. Навіть голий `raise NewError(...)` усередині блоку `except` зазвичай не втрачає оригінальний виняток — Python автоматично записує його як `__context__` і все ще показує в трасуванні з приміткою «During handling of the above exception, another exception occurred». `raise NewError(...) from original_error` йде далі: явно підвищує цей зв'язок до навмисного `__cause__`, формуючи трасування з чіткішим формулюванням «The above exception was the direct cause of the following exception», а `from None` — спосіб повністю придушити оригінал у тих рідкісних випадках, коли це справді потрібно.

Сильна відповідь включає

  • власні винятки дозволяють реагувати на конкретні доменні помилки
  • можуть нести структуровані дані, а не лише текст повідомлення
  • голий raise усередині except зберігає оригінал як __context__; raise ... from підвищує до явного __cause__

Практичні приклади

Обгортання низькорівневої помилки зі збереженням причини
class ConfigMissingError(RuntimeError):
    pass

def require_key(config, key):
    try:
        return config[key]
    except KeyError as exc:
        raise ConfigMissingError(f"Missing config key: {key}") from exc

require_key({}, "token")

Код, що викликає, може перехопити доменний ConfigMissingError, а `from exc` зберігає початковий KeyError як явну причину в traceback.

Очікуваний результат: Виникає ConfigMissingError, а KeyError('token') показано як його пряму причину.

#exceptions#error-handling

Джерела

Errors and ExceptionsPython Software Foundation · try/except/else/finally control flow and exception chainingThe Python Language ReferencePython Software Foundation · Precise scoping, evaluation order and expression semantics
Errors and context managersSeniorSpecialistTheory

Яку проблему вирішують групи винятків і синтаксис `except*`, введені в Python 3.11?

Відповідь

До Python 3.11 код, що виконував кілька незалежних операцій — наприклад, декілька паралельних задач, — міг поширювати лише один виняток за раз, змушуючи або зупинятися на першій помилці, або вручну збирати помилки у список самостійно. `ExceptionGroup` об'єднує кілька непов'язаних винятків, викликаних разом, в один об'єкт, а синтаксис `except*` (зі зіркою) дозволяє обробнику зіставляти й обробляти лише ті винятки всередині групи, типи яких йому цікаві, потенційно обробляючи різні типи винятків з тієї самої групи в різних гілках `except*`, тоді як решта поширюється далі.

Сильна відповідь включає

  • вирішує поширення кількох одночасних непов'язаних винятків
  • ExceptionGroup об'єднує кілька винятків в один об'єкт
  • except* зіставляє й обробляє конкретні типи винятків усередині групи

Практичний приклад

Виконання кількох перевірок валідації, кожна з яких може незалежно зазнати невдачі, може викликати одну `ExceptionGroup("validation failed", [ValueError(...), TypeError(...)])`, і `except* ValueError as eg:` обробляє лише члени ValueError.

#exceptions#python-3-11

Джерела

Errors and ExceptionsPython Software Foundation · try/except/else/finally control flow and exception chainingWhat's new in Python 3.14Python Software Foundation · Current-version language and runtime changes, including template strings and free-threading support
Errors and context managersMiddleVery commonTheory

Які методи потрібні класу для підтримки оператора `with`, і що контролюють їхні значення, що повертаються?

Відповідь

Менеджер контексту реалізує `__enter__`, що виконується при вході в блок `with` і чиє значення прив'язується до змінної `as`, та `__exit__(self, exc_type, exc_value, traceback)`, що завжди виконується при виході з блоку, незалежно від того, чи вийшли нормально, чи через виняток. Якщо виняток стався, `__exit__` отримує його деталі й може придушити його, повернувши істинне значення; повернення `None` чи хибного значення дозволяє винятку поширитися нормально після виконання очищення. Це гарантує виконання коду очищення навіть коли блок викликає виняток, на відміну від звичайного try/finally, яке викликач міг би забути написати.

Сильна відповідь включає

  • значення __enter__ прив'язується до змінної as
  • __exit__ завжди виконується, отримує деталі винятку, якщо він стався
  • повернення істинного значення з __exit__ придушує виняток

Практичний приклад

Вбудований `open()` повертає менеджер контексту, чий `__exit__` закриває файловий дескриптор, навіть якщо код усередині блоку `with` викликає виняток на півдорозі.

#context-managers#with-statement

Джерела

Data modelPython Software Foundation · Dunder methods, object identity, attribute lookup and the descriptor and context manager protocolscontextlib — Utilities for with-statement contextsPython Software Foundation · Context manager helpers, including contextmanager and ExitStack
Errors and context managersMiddleCommonPractical

Як `@contextlib.contextmanager` дозволяє писати менеджер контексту як функцію-генератор замість класу?

Відповідь

`@contextlib.contextmanager` обгортає функцію-генератор, що видає значення рівно один раз: усе перед `yield` стає логікою `__enter__`, а видане значення стає прив'язкою `as`. Якщо тіло `with` викликає виняток, він кидається назад у генератор саме в точці виразу `yield` — тож код, написаний після `yield`, гарантовано виконується як очищення, лише якщо обгорнутий у власний `try`/`finally` навколо `yield`, точно як довелося б зробити для пари `__enter__`/`__exit__`, написаної вручну. Канонічний патерн — `resource = acquire(); try: yield resource; finally: release(resource)`; пропуск `try`/`finally` означає, що виняток з тіла `with` може повністю пропустити код очищення, і це найпоширеніша помилка з цим декоратором. Зроблений правильно, він все ще дозволяє уникнути написання повноцінного класу з окремими методами `__enter__`/`__exit__` для простих пар налаштування/очищення.

Сильна відповідь включає

  • код перед yield — це __enter__, видане значення — прив'язка as
  • очищення після yield надійно виконується лише якщо обгорнуте у власний try/finally навколо yield
  • виняток з тіла with кидається назад у точці yield

Практичний приклад

`@contextmanager\ndef managed_file(path):\n f = open(path)\n try:\n yield f\n finally:\n f.close()` гарантує, що `f.close()` виконається, навіть якщо код усередині блоку `with` викликає виняток, бо `yield` обгорнутий у `try`/`finally`.

#context-managers#contextlib

Джерела

contextlib — Utilities for with-statement contextsPython Software Foundation · Context manager helpers, including contextmanager and ExitStack
Errors and context managersSeniorOccasionalTroubleshooting

Що станеться, якщо і блок `try`, і його блок `finally` містять оператор `return`?

Відповідь

`return` (чи `break`/`continue`) у блоці `finally` завжди перемагає: він тихо відкидає значення, що повертається з блоку `try`, а також тихо поглинає будь-який виняток, що поширювався з блоків `try`/`except`, бо потік керування, що виходить через `finally`, перекриває той, що виходив із секції `try`. Це майже завжди випадкова помилка, а не навмисне рішення дизайну, тому лінтери зазвичай позначають `return` усередині `finally`, а Python 3.14 додав власний `SyntaxWarning` компілятора для `return`, `break` чи `continue` усередині `finally` саме з цієї причини — патерн залишається дозволеним синтаксисом, але тепер сам інтерпретатор про нього попереджає.

Сильна відповідь включає

  • return у finally завжди перемагає, відкидаючи значення try
  • також тихо поглинає будь-який виняток, що поширювався
  • Python 3.14 додав SyntaxWarning компілятора для return/break/continue усередині finally

Практичний приклад

`def f():\n try:\n return 1\n finally:\n return 2` повертає 2, а `def g():\n try:\n raise ValueError()\n finally:\n return 3` повертає 3 і повністю поглинає ValueError.

#exceptions#gotchas#control-flow

Джерела

The Python Language ReferencePython Software Foundation · Precise scoping, evaluation order and expression semantics
Errors and context managersJuniorCommonTroubleshooting

Чому голий `except:` (чи `except Exception: pass`) вважається антипатерном?

Відповідь

Голий `except:` — небезпечніший з двох: він перехоплює кожен виняток без розбору, включно з підкласами `BaseException`, як-от `KeyboardInterrupt` і `SystemExit`, тож може поглинути користувацький Ctrl+C чи навмисний `sys.exit()` поряд зі справжніми багами. `except Exception: pass` вужчий — `KeyboardInterrupt` і `SystemExit` успадковуються напряму від `BaseException`, а не від `Exception`, тож він їх не перехоплює, — але все ще без розбору перехоплює кожну звичайну помилку, включно з програмними помилками на кшталт `TypeError` чи `AttributeError` через справжній баг, приховуючи реальні дефекти за тихим «успіхом» замість того, щоб їх виявити. У поєднанні з `pass` обидві форми активно знищують діагностичну інформацію (трасування), яка допомогла б знайти реальну проблему. Правильна практика — перехоплювати найвужчий тип винятку, з якого код може змістовно відновитися, а якщо справді потрібне ширше логування — логувати повний виняток з трасуванням перед тим, як вирішувати, чи повторно його кидати.

Сильна відповідь включає

  • голий except: перехоплює й підкласи BaseException як KeyboardInterrupt/SystemExit; except Exception: — ні
  • обидві форми разом з pass знищують діагностичну інформацію трасування
  • правильна практика — перехоплювати найвужчий тип, з якого можна відновитися

Практичний приклад

`try:\n save_to_db(record)\nexcept:\n pass` може місяцями приховувати справжню `TypeError` через одруківку в коді, бо помилка ніде не проявляється.

#exceptions#anti-patterns

Джерела

Errors and ExceptionsPython Software Foundation · try/except/else/finally control flow and exception chainingPEP 8 — Style Guide for Python CodePython Software Foundation · Naming, layout and readability conventions
Errors and context managersMiddleOccasionalPractical

Як можна відкрити кілька ресурсів в одному операторі `with`, і що додає `contextlib.ExitStack` для динамічної їх кількості?

Відповідь

Один оператор `with` може перелічувати кілька менеджерів контексту через кому, або в дужках на кількох рядках з Python 3.10: `with open("a") as a, open("b") as b:`, і Python гарантує, що всі вони закриються у зворотному порядку, навіть якщо один з них викликає виняток на півдорозі налаштування. `contextlib.ExitStack` обробляє випадок, коли кількість менеджерів контексту невідома до часу виконання — можна викликати `stack.enter_context(cm)` у циклі, і `ExitStack` все ще гарантує, що кожен успішно відкритий менеджер контексту буде коректно закритий у зворотному порядку, навіть якщо пізніший зазнає невдачі.

Сильна відповідь включає

  • with через кому чи в дужках обробляє фіксований відомий набір
  • усі закриваються у зворотному порядку навіть при частковій невдачі
  • ExitStack обробляє динамічну, визначену під час виконання кількість

Практичний приклад

`with ExitStack() as stack:\n files = [stack.enter_context(open(p)) for p in paths]` безпечно відкриває довільну, визначену під час виконання кількість файлів і гарантує, що всі вони закриються.

#context-managers#contextlib

Джерела

contextlib — Utilities for with-statement contextsPython Software Foundation · Context manager helpers, including contextmanager and ExitStack
Errors and context managersMiddleCommonDesign

Що означають EAFP і LBYL, і чому ідіоматичний Python зазвичай надає перевагу EAFP?

Відповідь

LBYL («подивись, перш ніж стрибнути») перевіряє передумови перед дією, наприклад `if key in d: value = d[key]`. EAFP («легше просити пробачення, ніж дозволу») просто намагається виконати операцію всередині `try` й обробляє виняток, якщо вона зазнає невдачі, наприклад `try: value = d[key] except KeyError: ...`. Python надає перевагу EAFP, бо перевірка й наступна дія не атомарні — стан може змінитися між перевіркою і використанням у паралельному коді, спричиняючи стан гонки, а також бо EAFP уникає виконання роботи пошуку чи валідації двічі (раз для перевірки, раз для використання), що часто і простіше, і швидше в типовому випадку, коли операція успішна.

Сильна відповідь включає

  • LBYL перевіряє спочатку, EAFP намагається й обробляє невдачу
  • перевірка-потім-дія не атомарна, може спричинити стан гонки
  • EAFP уникає подвійної роботи в типовому успішному випадку

Практичний приклад

`try:\n value = cache[key]\nexcept KeyError:\n value = compute_and_store(key)` уникає окремої перевірки `if key in cache:`, яка інакше могла б конфліктувати з іншим потоком, що видаляє ключ між перевіркою і читанням.

#exceptions#design#eafp

Джерела

The Python TutorialPython Software Foundation · Baseline syntax, control flow, functions and module basicsPEP 20 — The Zen of PythonPython Software Foundation · Design philosophy behind idiomatic Python choices
Concurrency and parallelismMiddleVery commonTheory

Що таке глобальне блокування інтерпретатора (GIL), і чому воно існує в типовій збірці CPython?

Відповідь

GIL — це єдиний м'ютекс усередині типового інтерпретатора CPython, що дозволяє лише одному потоку виконувати байткод Python одночасно, навіть на машині з кількома ядрами CPU. Він існує, бо керування пам'яттю CPython, зокрема підрахунок посилань, що використовується для збирання сміття, за замовчуванням не є потокобезпечним, і GIL був простим та ефективним способом уникнути гонок даних на лічильнику посилань кожного об'єкта без потреби в дрібнозернистому блокуванні по всьому інтерпретатору. Практичний наслідок — чистий Python-код, прив'язаний до CPU, у багатопотоковому виконанні не отримує реального паралельного прискорення на кількох ядрах, хоча потоки, прив'язані до вводу-виводу, все ще виграють, бо GIL звільняється під час блокуючих операцій вводу-виводу.

Сильна відповідь включає

  • єдиний м'ютекс, що дозволяє лише одному потоку виконувати байткод
  • існує, бо підрахунок посилань не потокобезпечний без нього
  • CPU-прив'язані потоки не паралелізуються, I/O-прив'язані все ще виграють

Практичний приклад

Чотири потоки Python, кожен з яких виконує чисту CPU-обчислювальну роботу на 4-ядерній машині, зазвичай працюють ледь швидше за один потік, тоді як чотири потоки, що чекають на мережеві запити, можуть реально перекривати час очікування.

#gil#concurrency#cpython-internals

Джерела

Python support for free threadingPython Software Foundation · The GIL, the free-threaded build and its compatibility trade-offsPEP 703 — Making the Global Interpreter Lock OptionalPython Software Foundation · The technical proposal behind the free-threaded CPython build
Concurrency and parallelismMiddleVery commonDesign

Як обрати між `threading` та `multiprocessing` для конкретного навантаження?

Відповідь

Використовуйте `threading` для роботи, прив'язаної до вводу-виводу — мережеві виклики, доступ до файлів, запити до бази даних — де більшість часу витрачається на очікування, бо в типовій збірці CPython з GIL він звільняється під час блокуючого вводу-виводу, а потоки легкі у створенні зі спільною пам'яттю. Використовуйте `multiprocessing` для роботи, прив'язаної до CPU — важкі обчислення, обробка зображень, обробка даних — бо кожен процес отримує власний інтерпретатор Python і власний GIL, даючи справжнє паралельне виконання на кількох ядрах, ціною вищих накладних витрат на запуск, відсутності спільної пам'яті за замовчуванням і вимоги, щоб дані, що передаються між процесами, були придатні для pickle. Цей поділ — типова евристика для стандартної збірки CPython, а не абсолютне правило: офіційно підтримувана free-threaded збірка Python 3.14 дозволяє й потокам, прив'язаним до CPU, справді паралелізуватися, а багато нативних розширень (NumPy та інші C-прискорені бібліотеки) звільняють GIL під час власної CPU-важкої роботи навіть у стандартній збірці.

Сильна відповідь включає

  • threading для I/O-роботи, GIL звільняється при блокуючих викликах у стандартній збірці
  • multiprocessing для CPU-роботи, окремий інтерпретатор і GIL на процес
  • поділ — евристика для стандартної збірки, не абсолют — free-threaded 3.14 і розширення, що звільняють GIL, є винятками

Практичні приклади

Вибір executor за типом навантаження
from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor

def fetch(url):
    ...  # blocking network I/O

def crunch(number):
    return sum(i * i for i in range(number))

with ThreadPoolExecutor(max_workers=8) as pool:
    io_results = list(pool.map(fetch, urls))

with ProcessPoolExecutor() as pool:
    cpu_results = list(pool.map(crunch, numbers))

Threads зазвичай підходять для блокуючого I/O, бо очікування можна дешево перекривати. Processes — типовий вибір для pure-Python CPU роботи у стандартному CPython, бо кожен процес має власний інтерпретатор.

Очікуваний результат: Приклад показує вибір моделі; fetch/urls/numbers мають бути задані застосунком.

#threading#multiprocessing#design

Джерела

threading — Thread-based parallelismPython Software Foundation · Locks, thread safety and thread-local statemultiprocessing — Process-based parallelismPython Software Foundation · Process pools, pickling constraints and inter-process communicationPython Interview Questions and AnswersToptal · Senior-level coverage signal for concurrency, testability and design-quality questions
Concurrency and parallelismSeniorVery commonTheory

Як цикл подій asyncio досягає конкурентності одним потоком, і що робить функцію корутиною?

Відповідь

Функція, визначена через `async def`, — функція-корутина; її виклик повертає об'єкт-корутину, який нічого не робить, доки його не очікують (await) чи не заплановано як задачу. Цикл подій виконує одну корутину за раз в одному потоці, і щоразу, коли ця корутина натрапляє на `await` чогось, що заблокувало б (як мережевий ввід-вивід), вона добровільно повертає керування циклу, який тоді виконує іншу готову корутину тим часом. Це кооперативна багатозадачність: конкурентність виникає з того, що багато корутин по черзі проходять точки `await`, а не з паралельного виконання, тож корутина, яка ніколи не робить await (чиста обчислювальна робота), блокує весь цикл подій для всіх.

Сильна відповідь включає

  • async def створює функцію-корутину, виклик просто повертає об'єкт
  • цикл виконує одну корутину за раз, перемикається в точках await
  • кооперативно, не паралельно — некорутина, що не робить await, блокує все

Практичні приклади

Конкурентний I/O через asyncio.gather
import asyncio

async def fetch(name, delay):
    await asyncio.sleep(delay)
    return name

async def main():
    results = await asyncio.gather(
        fetch("a", 0.2),
        fetch("b", 0.1),
    )
    print(results)

asyncio.run(main())

Кожна coroutine віддає керування на await, дозволяючи event loop виконати іншу готову coroutine. gather зберігає порядок вхідних задач у результаті, навіть якщо b завершується першою.

Очікуваний результат: ['a', 'b'] приблизно через 0.2 секунди замість 0.3 секунди послідовного очікування.

#asyncio#coroutines#event-loop

Джерела

asyncio — Asynchronous I/OPython Software Foundation · Event loop, coroutines, tasks and structured concurrency primitivesPEP 492 — Coroutines with async and await syntaxPython Software Foundation · The origin of async/await syntax and native coroutine objects
Concurrency and parallelismSeniorCommonTroubleshooting

Як стан гонки все ще може виникнути у потоках Python попри GIL, і як `threading.Lock` йому запобігає?

Відповідь

Складена операція на кшталт `counter += 1` насправді є послідовністю читання-зміни-запису (завантажити старе значення, додати одиницю, зберегти нове), а не одним неподільним кроком, тож два потоки можуть обидва прочитати те саме старе значення до того, як хтось із них запише збільшений результат, і одне збільшення тихо втрачається — класична несинхронізована гонка незалежно від того, як саме ця послідовність реалізована внутрішньо. У типовій збірці CPython з GIL лише один потік виконує байткод Python за раз, але інтерпретатор все ще може перемкнути, який потік виконується, між окремими кроками цієї послідовності, тож складена операція в цілому не захищена. `threading.Lock` (використаний як `with lock:`) гарантує, що лише один потік може виконувати захищений блок за раз, роблячи всю послідовність читання-зміни-запису фактично атомарною з погляду інших потоків, що поважають те саме блокування — і це виправлення потрібне незалежно від наявності GIL, що важливо, бо free-threaded збірка Python 3.14 повністю прибирає GIL і потребує саме такого явного блокування ще більше, ніж раніше.

Сильна відповідь включає

  • складені операції читання-зміни-запису на кшталт += — не один неподільний крок
  • інтерпретатор може перемкнути потоки між окремими кроками, втративши оновлення
  • Lock серіалізує доступ, роблячи послідовність атомарною незалежно від наявності GIL

Практичні приклади

Захист спільного стану через lock
from threading import Lock, Thread

counter = 0
lock = Lock()

def increment_many():
    global counter
    for _ in range(10_000):
        with lock:
            counter += 1

threads = [Thread(target=increment_many) for _ in range(4)]
for thread in threads:
    thread.start()
for thread in threads:
    thread.join()

print(counter)

Lock робить read-modify-write оновлення критичною секцією, спільною для всіх потоків. Не слід вважати GIL заміною прикладної синхронізації.

Очікуваний результат: 40000.

#threading#race-conditions#locks

Джерела

threading — Thread-based parallelismPython Software Foundation · Locks, thread safety and thread-local state
Concurrency and parallelismSeniorOccasionalTroubleshooting

Що спричиняє взаємне блокування (deadlock) між двома потоками, і які стандартні способи його уникнути?

Відповідь

Класичне взаємне блокування трапляється, коли потік A тримає блокування 1 і чекає на блокування 2, тоді як потік B тримає блокування 2 і чекає на блокування 1 в той самий час; жоден не може продовжити, й обидва чекають вічно. Стандартні стратегії запобігання: завжди захоплювати кілька блокувань у тому самому глобально узгодженому порядку в усіх шляхах коду, використовувати тайм-аут на захоплення блокування (`lock.acquire(timeout=5)`), щоб застряглий потік врешті здався й міг відновитися чи повідомити про помилку, або перебудувати код так, щоб взагалі не тримати більше одного блокування одночасно.

Сильна відповідь включає

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

Практичний приклад

`transfer(account_a, account_b)`, що блокує `account_a`, а потім `account_b`, тоді як паралельний `transfer(account_b, account_a)` блокує `account_b`, а потім `account_a`, може призвести до взаємного блокування; блокування рахунків у фіксованому порядку (наприклад, за id рахунку) уникає цього.

#threading#deadlock#locks

Джерела

threading — Thread-based parallelismPython Software Foundation · Locks, thread safety and thread-local state
Concurrency and parallelismMiddleCommonPractical

Що надає модуль `concurrent.futures`, і чим відрізняється використання `ThreadPoolExecutor` і `ProcessPoolExecutor`?

Відповідь

`concurrent.futures` дає уніфікований API вищого рівня для передачі викличних об'єктів у пул воркерів і отримання назад об'єктів `Future`, що представляють результати, які ще обчислюються, замінюючи багато ручного шаблонного коду керування потоками чи процесами. `ThreadPoolExecutor` і `ProcessPoolExecutor` мають той самий базовий інтерфейс `Executor`/`Future` — `submit()`, `map()` та `as_completed()` — тож перехід між виконанням на основі потоків і процесів для конкретного навантаження часто невелика зміна, хоча кожен виконавець зберігає власні обмеження, яких спільний інтерфейс не усуває (придатні для pickle аргументи й значення, що повертаються, для процесів; відсутність справжньої CPU-паралельності для потоків у стандартному інтерпретаторі з GIL). Чи потоки реально отримують CPU-паралельність, залежить від збірки інтерпретатора: типова збірка CPython з GIL не дає потокам справжнього паралельного виконання CPU, а офіційно підтримувана free-threaded збірка Python 3.14 — дає.

Сильна відповідь включає

  • уніфікований API, що повертає об'єкти Future з пулу воркерів
  • ThreadPoolExecutor і ProcessPoolExecutor мають спільний базовий інтерфейс, але власні обмеження
  • CPU-паралельність потоків залежить від збірки інтерпретатора — стандартна з GIL проти free-threaded 3.14

Практичний приклад

`with ThreadPoolExecutor(max_workers=8) as pool:\n results = list(pool.map(fetch_url, urls))` стає паралельним за CPU просто заміною `ThreadPoolExecutor` на `ProcessPoolExecutor`, якщо `fetch_url` і його аргументи придатні для pickle.

#concurrent-futures#threading#multiprocessing

Джерела

threading — Thread-based parallelismPython Software Foundation · Locks, thread safety and thread-local statemultiprocessing — Process-based parallelismPython Software Foundation · Process pools, pickling constraints and inter-process communication
Concurrency and parallelismSeniorOccasionalTheory

Що таке безблокувальний (free-threaded) Python, і який його поточний статус станом на Python 3.14?

Відповідь

Free-threaded Python — це збірка CPython, запропонована PEP 703, що повністю прибирає GIL, тож кілька потоків можуть виконувати байткод Python справді паралельно на кількох ядрах, використовуючи натомість дрібнозернистіше внутрішнє блокування та іншу схему керування пам'яттю. PEP 779 визначив критерії для офіційної підтримки цієї збірки, і Python 3.14 відповів цим критеріям, зробивши free-threading офіційно підтримуваним вперше — але це залишається опційною альтернативною збіркою, а не типовою, бо вона несе витрати продуктивності в однопотоковому режимі, і багато пакетів з C-розширеннями ще не повністю сумісні з нею.

Сильна відповідь включає

  • збірка PEP 703 прибирає GIL для справжнього паралельного виконання байткоду
  • PEP 779 визначив критерії; 3.14 зробив офіційно підтримуваним
  • все ще опційно, не типово, через витрати в однопотоковому режимі й сумісність розширень

Практичний приклад

Запуск `python3.14t` (free-threaded збірки) дозволяє пулу потоків, прив'язаних до CPU, реально масштабуватися на кілька ядер — те, що неможливо в стандартній збірці з GIL, але стороннє розширення без підтримки free-threading може автоматично повторно увімкнути GIL при імпорті.

#gil#free-threading#python-3-14

Джерела

PEP 703 — Making the Global Interpreter Lock OptionalPython Software Foundation · The technical proposal behind the free-threaded CPython buildPEP 779 — Criteria for supported status for free-threaded PythonPython Software Foundation · The criteria that made free-threaded Python officially supported starting with 3.14Python support for free threadingPython Software Foundation · The GIL, the free-threaded build and its compatibility trade-offs
Concurrency and parallelismSeniorOccasionalPractical

Чим `asyncio.gather()` відрізняється від `asyncio.wait()` при паралельному виконанні кількох корутин?

Відповідь

`asyncio.gather(*coros)` планує всі корутини для паралельного виконання й повертає їхні результати у вигляді списку в тому самому порядку, в якому корутини були передані, коли всі завершаться. З типовим `return_exceptions=False` виняток першої корутини, що зазнала невдачі, одразу поширюється до викликача, але інші корутини, передані в той самий виклик `gather()`, не скасовуються автоматично — вони продовжують виконуватись у фоні, якщо щось інше їх не скасує, наприклад якщо викликач скасує сам виклик `gather()`. Це відрізняється від `asyncio.TaskGroup` (Python 3.11+), яка справді скасовує сусідні задачі, щойно одна з них падає з чимось, відмінним від скасування. `asyncio.wait(tasks)` натомість повертає два набори, `(done, pending)`, не кидаючи винятків самостійно й не зберігаючи порядок введення, даючи тонший контроль над частковим завершенням, власною обробкою тайм-ауту й поверненням щойно перша задача завершиться, якщо налаштовано з `return_when=FIRST_COMPLETED`.

Сильна відповідь включає

  • gather повертає впорядковані результати; за замовчуванням перший виняток поширюється, але сусідні корутини не скасовуються автоматично
  • TaskGroup, на відміну від gather, скасовує сусідні задачі при невдачі, що не є скасуванням
  • wait повертає набори (done, pending) без кидання винятків, порядок не зберігається

Практичний приклад

`results = await asyncio.gather(fetch(1), fetch(2), fetch(3))` дає `[result1, result2, result3]` саме в такому порядку, тоді як `done, pending = await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED)` повертається, щойно будь-яка одна задача завершиться.

#asyncio#coroutines

Джерела

asyncio — Asynchronous I/OPython Software Foundation · Event loop, coroutines, tasks and structured concurrency primitives
Concurrency and parallelismSeniorOccasionalTheory

Чи є вбудовані структури даних Python, як-от `list` і `dict`, потокобезпечними?

Відповідь

Окремі вбудовані операції, що відповідають одній інструкції байткоду, як `list.append()` чи одне присвоєння ключа словника, фактично атомарні в типовій збірці CPython з GIL і не пошкодять базову структуру даних — хоча це ніколи не було формальною гарантією на рівні мови, лише випадковим наслідком того, як працює еталонна реалізація. У free-threaded збірці Python 3.14 GIL взагалі немає, і вбудовані контейнери натомість покладаються на власне внутрішнє дрібнозернисте блокування, щоб залишатися структурно безпечними, тож механізм відрізняється, хоча проста атомарність окремої операції загалом зберігається. У будь-якому разі це не робить тип «потокобезпечним» у загальному сенсі: будь-яка послідовність з кількох операцій, що має відбутися як одна логічна одиниця, наприклад «перевірити, чи існує ключ, потім вставити, якщо ні», не атомарна в цілому й все ще підвладна станам гонки між окремими кроками. Рекомендована практика — визначити реальний інваріант, потрібний коду, і захистити його явною синхронізацією: `Lock`, чергою чи чітким єдиним власником доступу, а не покладатися на випадкову атомарність будь-якого інтерпретатора.

Сильна відповідь включає

  • однокрокові операції фактично атомарні в стандартній збірці з GIL, але це ніколи не було формальною гарантією
  • free-threaded 3.14 використовує власне внутрішнє блокування замість GIL для цього
  • багатокрокові логічні операції ніколи не атомарні в цілому; захищайте реальні інваріанти явними блокуваннями

Практичний приклад

`if key not in cache: cache[key] = compute()` все ще може виконати `compute()` двічі з двох потоків, бо перевірка й присвоєння — два окремі атомарні кроки, а не один.

#threading#race-conditions

Джерела

threading — Thread-based parallelismPython Software Foundation · Locks, thread safety and thread-local stateData modelPython Software Foundation · Dunder methods, object identity, attribute lookup and the descriptor and context manager protocols
Concurrency and parallelismMiddleCommonTroubleshooting

Чому аргументи й значення, що повертаються, у коді `multiprocessing` мають бути придатними для pickle, і що часто не відповідає цій вимозі?

Відповідь

Оскільки кожен робочий процес має повністю окрему пам'ять від батьківського, `multiprocessing` не може ділити об'єкти Python напряму через межу процесу; натомість він серіалізує (pickle) цільову функцію, її аргументи й значення, що повертається, щоб передати їх між процесами, і десеріалізує на іншому боці. Поширені речі, що не проходять pickle: lambda, локально визначені вкладені функції та класи, відкриті файлові дескриптори, підключення до бази даних та непридатні для pickle сторонні об'єкти, як-от деякі дескриптори GUI чи драйверів — їх потрібно замінити функціями/класами рівня модуля чи повторно створити всередині робочого процесу, а не передавати напряму.

Сильна відповідь включає

  • окрема пам'ять процесу означає, що об'єкти треба серіалізувати через межу
  • multiprocessing серіалізує функцію, аргументи й значення, що повертається
  • lambda, локальні функції/класи, відкриті дескриптори часто не проходять pickle

Практичний приклад

`pool.map(lambda x: x * 2, data)` викликає `PicklingError` під `multiprocessing`, тоді як заміна lambda на функцію рівня модуля `def double(x): return x * 2` працює коректно.

#multiprocessing#pickling#gotchas

Джерела

multiprocessing — Process-based parallelismPython Software Foundation · Process pools, pickling constraints and inter-process communication
Concurrency and parallelismMiddleCommonDesign

Як визначити, чи навантаження прив'язане до CPU, чи до вводу-виводу, і чому ця класифікація визначає вибір моделі конкурентності?

Відповідь

Навантаження прив'язане до CPU, якщо його час виконання визначається обчисленнями, які процесор реально має виконати — розбір, математика, трансформації зображень, і профілювання показує стабільно високе використання CPU. Воно прив'язане до вводу-виводу, якщо більшість часу виконання витрачається на очікування чогось зовнішнього — мережевої відповіді, доступу до диска, іншого сервісу — з CPU здебільшого простоюючим під час цих очікувань. Ця класифікація важлива, бо в типовій збірці CPython з GIL потоки й корутини asyncio допомагають лише роботі, прив'язаній до I/O, оскільки GIL не дозволяє коду Python, прив'язаному до CPU, на потоках виконуватися справді паралельно; робота, прив'язана до CPU, у цій збірці потребує `multiprocessing` чи вивантаження в C-розширення/нативну бібліотеку, що звільняє GIL, щоб реально пришвидшитися на кількох ядрах. Офіційно підтримувана free-threaded збірка Python 3.14 змінює це саме для потоків, прив'язаних до CPU, дозволяючи їм масштабуватися на кілька ядер без multiprocessing, хоча вона залишається опційною альтернативною збіркою, а не типовою.

Сильна відповідь включає

  • CPU-прив'язане: час визначається реальними обчисленнями, високе використання CPU
  • I/O-прив'язане: час визначається очікуванням зовнішнього, CPU здебільшого простоює
  • у стандартній збірці з GIL лише multiprocessing/нативний код паралелізують CPU-роботу; free-threaded 3.14 — виняток

Практичний приклад

Розбір і валідація мільйона записів JSON у пам'яті — CPU-прив'язана робота, що потребує `multiprocessing` для пришвидшення; завантаження мільйона маленьких файлів через HTTP — I/O-прив'язана робота, що суттєво пришвидшується з `asyncio` чи пулом потоків.

#concurrency#design#performance

Джерела

asyncio — Asynchronous I/OPython Software Foundation · Event loop, coroutines, tasks and structured concurrency primitivesmultiprocessing — Process-based parallelismPython Software Foundation · Process pools, pickling constraints and inter-process communication
Memory management and performanceSeniorCommonTheory

Як CPython переважно керує пам'яттю, і яку проблему сам лише підрахунок посилань не вирішує?

Відповідь

У типовій збірці CPython кожен об'єкт несе лічильник посилань, який збільшується щоразу, коли створюється нове посилання на нього, і зменшується щоразу, коли посилання зникає; коли лічильник досягає нуля, пам'ять об'єкта звільняється негайно й детерміновано. Це обробляє більшість очищення пам'яті дешево й передбачувано для звичайних об'єктів, але не може звільнити об'єкти, що посилаються один на одного циклічно — як два об'єкти, що тримають посилання один на одного, — бо жоден з їхніх лічильників природно ніколи не досягає нуля, навіть коли жоден зовнішній об'єкт більше не посилається на жодного з них. Free-threaded збірка Python 3.14 послаблює частину цієї негайності: певні часто спільні об'єкти робляться «безсмертними» й ніколи не звільняються через підрахунок посилань, а відкладений, поточний за потоками підрахунок посилань цієї збірки в поєднанні з технікою QSBR (reclamation на основі станів спокою) означає, що пам'ять об'єкта не завжди звільняється в ту саму мить, коли його лічильник логічно досяг би нуля, обмінюючи частину цієї детермінованості на нижчі витрати міжпотокової синхронізації.

Сильна відповідь включає

  • лічильник посилань на об'єкт у стандартній збірці, звільнення негайне при нулі для звичайних об'єктів
  • не може самостійно звільнити циклічні посилання
  • free-threaded 3.14 використовує безсмертні об'єкти й відкладений підрахунок посилань за потоками (QSBR), послаблюючи сувору негайність

Практичний приклад

Два вузли двозв'язного списку, що посилаються один на одного як `next` і `prev`, ніколи самостійно не досягнуть лічильника посилань нуль, навіть після видалення самого списку, якщо щось інше не втрутиться, щоб розірвати цикл.

#memory-management#cpython-internals

Джерела

gc — Garbage collector interfacePython Software Foundation · Generational garbage collection and cyclic-reference behaviorThe Python Standard LibraryPython Software Foundation · Authoritative behavior for built-in types and standard library modules
Memory management and performanceSeniorOccasionalTheory

Як генераційний збирач сміття Python доповнює підрахунок посилань для обробки циклів?

Відповідь

Модуль `gc` виконує окремий збирач, що виявляє цикли, поверх підрахунку посилань, спеціально щоб знаходити й звільняти групи об'єктів, що посилаються один на одного, але недосяжні звідусіль ще в програмі. Він організовує об'єкти в три покоління залежно від того, скільки попередніх проходів збирання вони пережили; нові об'єкти починають у поколінні 0, яке збирається найчастіше, а об'єкти, що пережили збирання, підвищуються до старших поколінь, які скануються рідше, на емпірично обґрунтованому припущенні, що більшість об'єктів вмирають молодими, тоді як деякі живуть довго.

Сильна відповідь включає

  • окремий збирач циклів працює поряд з підрахунком посилань
  • три покоління, наймолодше збирається найчастіше
  • базується на припущенні, що більшість об'єктів вмирають молодими

Практичний приклад

Короткочасний список, побудований і відкинутий усередині функції, майже завжди очищується самим лише підрахунком посилань ще до того, як взагалі запуститься збирання покоління 0; довгоживучий словник-кеш врешті підвищується до покоління 2 й сканується рідко.

#garbage-collection#memory-management

Джерела

gc — Garbage collector interfacePython Software Foundation · Generational garbage collection and cyclic-reference behavior
Memory management and performanceSeniorOccasionalPractical

Що надає модуль `weakref`, і коли варто використовувати слабке посилання замість звичайного?

Відповідь

Слабке посилання дозволяє тримати вказівник на об'єкт, не збільшуючи його лічильник посилань, тож об'єкт, на який посилаються, все ще може бути зібраний сміттєзбирачем, ніби цього слабкого посилання не існувало; звернення до простроченого слабкого посилання дає `None` (чи викликає помилку для `weakref.proxy`) замість дійсного об'єкта. Це правильний інструмент для кешів, реєстрів спостерігачів/слухачів і зворотних посилань батько-дитина, де звичайне посилання або створило б циклічне посилання, яке збирач мусив би прибирати, або штучно тримало б об'єкт живим лише тому, що кеш чи список слухачів усе ще на нього посилається.

Сильна відповідь включає

  • слабкі посилання не збільшують лічильник посилань
  • звернення до простроченого слабкого посилання дає None
  • використовується для кешів, реєстрів спостерігачів, зворотних посилань

Практичний приклад

`self._parent = weakref.ref(parent)` у дочірньому вузлі дозволяє батьківському об'єкту звільнитися нормально, коли на нього більше ніхто не посилається, замість того щоб зворотне посилання дитини тримало його живим вічно.

#memory-management#weakref

Джерела

gc — Garbage collector interfacePython Software Foundation · Generational garbage collection and cyclic-reference behaviorThe Python Standard LibraryPython Software Foundation · Authoritative behavior for built-in types and standard library modules
Memory management and performanceMiddleCommonPerformance

Як знайти, яка функція реально повільна в програмі Python, перш ніж її оптимізувати?

Відповідь

Стандартний перший крок — `cProfile`, вбудований детермінований профайлер: запуск `python -m cProfile -s cumulative script.py` (чи обгортання коду `cProfile.run()`) інструментує кожен виклик функції й звітує кількість викликів, загальний час і накопичений час на функцію, що зазвичай виявляє реальне вузьке місце замість того, яке припускав розробник. Здогадуватися про продуктивність без попереднього вимірювання — відома пастка, бо реальний «гарячий» шлях часто перебуває в іншому місці, ніж підказує інтуїція, особливо коли задіяні кешування, ввід-вивід чи несподівано викликана допоміжна функція.

Сильна відповідь включає

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

Практичний приклад

`python -m cProfile -s cumulative slow_report.py` може виявити, що 90% часу виконання йде на єдиний допоміжний метод `format_currency()`, викликаний глибоко всередині циклу, а не на запит до бази даних, який усі вважали вузьким місцем.

#profiling#performance

Джерела

The Python Standard LibraryPython Software Foundation · Authoritative behavior for built-in types and standard library modules
Memory management and performanceSeniorOccasionalPerformance

Як вбудований модуль `tracemalloc` допомагає знайти витік пам'яті чи несподівано високе споживання пам'яті?

Відповідь

`tracemalloc` записує стек викликів Python за кожним виділенням пам'яті, яке сам Python робить через власні відстежувані домени пам'яті, після запуску через `tracemalloc.start()`, дозволяючи робити «знімки» використання пам'яті в різних точках життя програми й обчислювати різницю `snapshot2.compare_to(snapshot1, "lineno")`, яка ранжує, які саме рядки коду відповідальні за найбільше зростання виділеної пам'яті між двома точками, з налаштовуваною глибиною трасування. Це набагато цілеспрямованіше, ніж спостереження за загальною пам'яттю процесу в системному моніторі для відстеження зростання виділень на рівні Python, бо приписує зростання напряму рядкам вихідного коду Python. Проте він бачить не все, що використовує процес: виділення, зроблені нативними C-розширеннями чи бібліотеками поза власними відстежуваними доменами Python, і інші складові загальної пам'яті процесу (RSS), лишаються поза тим, що звітує `tracemalloc`, тож він доповнює, а не замінює системний профайлер пам'яті, коли підозра падає на нативну пам'ять.

Сильна відповідь включає

  • відстежує блоки, керовані Python, після запуску, не довільну нативну/процесну пам'ять
  • знімки в двох точках часу, порівнюються через compare_to(); глибина трасування налаштовується
  • не пояснює все зростання RSS/нативної пам'яті від C-розширень

Практичний приклад

Порівняння знімка, зробленого після запуску, зі знімком після обробки 10 000 запитів може точно вказати на єдиний рядок `cache[key] = large_object`, що ніколи не видаляє записи, як джерело повільного витоку пам'яті.

#profiling#memory-management

Джерела

The Python Standard LibraryPython Software Foundation · Authoritative behavior for built-in types and standard library modules
Memory management and performanceMiddleCommonPerformance

Як `functools.lru_cache` пришвидшує функцію, і які обмеження це накладає на її аргументи?

Відповідь

`@functools.lru_cache(maxsize=128)` обгортає функцію так, щоб кожна унікальна комбінація аргументів обчислювалась один раз, а її результат зберігався у внутрішньому словнику з ключем за цими аргументами; наступні виклики з тими самими аргументами миттєво повертають закешований результат замість повторного обчислення, витісняючи найдавніше використаний запис після перевищення `maxsize`. Оскільки кеш індексується за самими аргументами, кожен аргумент має бути хешованим — передача аргументу `list` чи `dict` викликає `TypeError` — і функція має бути чистою (її результат має залежати лише від аргументів, без побічних ефектів чи зовнішнього стану), інакше закешовані результати тихо стануть застарілими чи неправильними.

Сильна відповідь включає

  • кешує результати за комбінацією аргументів, витісняє найдавніше використаний понад maxsize
  • аргументи мають бути хешовані, list/dict викликають TypeError
  • функція має бути чистою, інакше кеш стане застарілим

Практичний приклад

`@lru_cache(maxsize=None)\ndef fib(n): return n if n < 2 else fib(n-1) + fib(n-2)` перетворює наївний рекурсивний розрахунок Фібоначчі з експоненційним часом на лінійний, ніколи не обчислюючи те саме `n` двічі.

#performance#functools#caching

Джерела

functools — Higher-order functions and operations on callable objectsPython Software Foundation · lru_cache, partial, wraps, reduce and singledispatch behavior
Memory management and performanceMiddleCommonTheory

Чому типовий код Python повільніший за еквівалентний код C, окрім «Python — інтерпретована мова»?

Відповідь

Окрім накладних витрат інтерпретації байткоду, кожне значення Python — це виділений у купі, підрахований за посиланнями об'єкт (навіть маленькі цілі числа), тож проста арифметика включає обхід вказівників і динамічну диспетчеризацію замість сирих регістрів CPU й машинних інструкцій. Динамічна типізація означає, що операція на кшталт `a + b` зазвичай потребує визначення реалізації на основі типів під час виконання, а не резолвиться компілятором один раз заздалегідь — хоча сучасний CPython (з 3.11) використовує внутрішній спеціалізуючий адаптивний інтерпретатор, що кешує розв'язану операцію для повторюваних місць байткоду, тож гарячий цикл, що повторно виконує той самий тип додавання, помітно швидший, ніж припускала б наївна модель пошуку на кожну операцію, без буквального пошуку методу `__add__` щоразу. Саме ці накладні витрати динамічного об'єкта, пом'якшені, але не усунені спеціалізацією, — причина, чому критичний за продуктивністю код Python зазвичай делегує гарячий внутрішній цикл C-розширенню (NumPy, Cython, прив'язки до Rust), а не намагається переоптимізувати інтерпретатор чистим Python.

Сильна відповідь включає

  • кожне значення — виділений у купі, підрахований об'єкт, навіть цілі числа
  • динамічна типізація означає розв'язання оператора зазвичай на кожен виклик, не один раз при компіляції
  • CPython 3.11+ спеціалізує гарячі місця байткоду внутрішньо, пом'якшуючи, але не усуваючи витрати

Практичний приклад

Чистий цикл Python, що підсумовує мільйон чисел з плаваючою комою, зазвичай на порядок повільніший за еквівалентний виклик `numpy.sum()`, бо NumPy виконує підсумовування скомпільованою мовою C над суцільним плоским буфером пам'яті.

#performance#cpython-internals

Джерела

dis — Disassembler for Python bytecodePython Software Foundation · Bytecode execution model used to reason about interpreter performanceData modelPython Software Foundation · Dunder methods, object identity, attribute lookup and the descriptor and context manager protocols
Memory management and performanceSeniorOccasionalPerformance

Які практичні способи пришвидшити справді гарячий цикл Python, коли профілювання підтвердило, що це вузьке місце?

Відповідь

Спершу профілюйте, і повторно профілюйте після кожної зміни — інтуїція про те, що повільне, часто помиляється, а оптимізація, що допомогла на одній версії Python, може бути нейтральною чи навіть шкідливою на новішій, бо сучасний спеціалізуючий CPython (3.11+) уже оптимізує деякі патерни внутрішньо. Найнадійніші виграші зазвичай алгоритмічні: зменшення обсягу роботи на рівні Python на елемент, заміна ручного циклу вбудованою реалізацією на C там, де семантика справді збігається (`sum()`, `any()`, list comprehension замість написаного вручну циклу з append), винесення роботи з циклу, коли вона дає однаковий результат щоразу, і пакетна обробка чисельної роботи векторизованою бібліотекою на кшталт NumPy замість циклу на чистому Python. Старіші мікротрюки на кшталт попередньої прив'язки часто використовуваного методу чи глобальної змінної до локального імені перед гарячим циклом все ще можуть допомогти в деяких випадках, але їхня користь залежить від версії й навантаження, а не є гарантованим стандартним виграшем — завжди перевіряйте бенчмарком конкретну зміну на цільовій версії Python, а не припускайте, що вона допомогла.

Сильна відповідь включає

  • спершу профілювання, повторне профілювання після кожної зміни, а не припущення про користь
  • алгоритмічні зміни й вбудовані реалізації на C — найнадійніші виграші
  • старіші мікрооптимізації залежать від версії й навантаження, не гарантовані на сучасному спеціалізуючому CPython

Практичний приклад

Заміна `result = []\nfor x in data:\n result.append(transform(x))` на `result = [transform(x) for x in data]` одночасно ідіоматичніша й вимірювано швидша, бо comprehension уникає повторного пошуку атрибута `.append`.

#performance#optimization

Джерела

dis — Disassembler for Python bytecodePython Software Foundation · Bytecode execution model used to reason about interpreter performanceThe Python Standard LibraryPython Software Foundation · Authoritative behavior for built-in types and standard library modules
Memory management and performanceMiddleOccasionalPractical

Що повертає вбудована функція `id()`, і як вона допомагає налагодити несподіване псевдонімування чи проблему з пам'яттю?

Відповідь

`id(obj)` повертає ціле число, гарантовано унікальне й незмінне для цього об'єкта протягом усього його життя; конкретно в CPython воно збігається з адресою пам'яті об'єкта, хоча це деталь реалізації, а не гарантія мови. Порівняння `id(a) == id(b)` (еквівалентне `a is b`) під час налагодження може підтвердити, чи дві змінні, що виглядають рівними, справді той самий об'єкт у пам'яті, а це саме те питання, що лежить за більшістю помилок псевдонімування, як-от несподівано спільний змінюваний стан між тим, що мало бути незалежними копіями.

Сильна відповідь включає

  • id() повертає унікальний, незмінний ідентифікатор на все життя об'єкта
  • у CPython збігається з адресою пам'яті, але це деталь реалізації
  • порівняння id підтверджує, чи змінні ділять той самий базовий об'єкт

Практичний приклад

`print(id(config_a), id(config_b))`, що друкує однакове число, коли очікувалися два «окремі» об'єкти конфігурації, швидко підтверджує, що відсутній виклик `.copy()` спричинив небажане спільне використання.

#debugging#identity

Джерела

Data modelPython Software Foundation · Dunder methods, object identity, attribute lookup and the descriptor and context manager protocols
Memory management and performanceSeniorOccasionalPerformance

Окрім `__slots__`, які ще техніки зменшують використання пам'яті для великих колекцій даних у Python?

Відповідь

Модуль `array` зберігає велику послідовність одного примітивного числового типу в компактному, суцільному буфері у стилі C замість окремих «упакованих» об'єктів Python, суттєво скорочуючи пам'ять порівняно зі `list` з мільйона окремих об'єктів `int`. Генератори, розглянуті деінде для лінивості, також знижують пікове споживання пам'яті, бо ніколи не матеріалізують всю послідовність одразу. Для структурованих записів `__slots__` (чи `@dataclass(slots=True)` з Python 3.10) прибирає накладні витрати `__dict__` на екземпляр, а для справді чисельних навантажень масиви NumPy зберігають елементи в єдиному упакованому буфері з набагато меншими накладними витратами на елемент, ніж будь-який чистий Python-контейнер.

Сильна відповідь включає

  • модуль array упаковує примітиви в компактний суцільний буфер
  • генератори уникають матеріалізації всієї послідовності одразу
  • slots (чи dataclass slots=True) і масиви NumPy знижують витрати на елемент

Практичний приклад

Зберігання десяти мільйонів показань датчика як `array.array("d", readings)` використовує приблизно вісім байтів на значення, тоді як звичайний `list` об'єктів `float` Python використовує в кілька разів більше через накладні витрати на об'єкт і непряму адресацію через вказівники.

#performance#memory-management

Джерела

The Python Standard LibraryPython Software Foundation · Authoritative behavior for built-in types and standard library modulesdataclasses — Data ClassesPython Software Foundation · Generated dunder methods, field defaults and frozen/eq/order semantics
Modules, packaging and environmentsMiddleCommonTheory

Як Python знаходить модуль, коли ви пишете `import something`, і яку роль відіграє `sys.path`?

Відповідь

При `import something` Python спершу перевіряє `sys.modules`, чи модуль уже імпортовано й закешовано; якщо ні — шукає в каталогах, перелічених у `sys.path`, по черзі, доки не знайде відповідний модуль чи пакет. `sys.path` починається з каталогу запущеного скрипта (чи поточного каталогу), потім записи зі змінної середовища `PYTHONPATH`, потім стандартна бібліотека й встановлені розташування site-packages; перемагає перший знайдений відповідний файл, саме тому локально названий файл, що затінює ім'я стандартної бібліотеки чи встановленого модуля (наприклад, `random.py` у власному проєкті), може тихо зламати непов'язані імпорти.

Сильна відповідь включає

  • спершу перевіряє кеш sys.modules, потім шукає каталоги sys.path по черзі
  • sys.path включає каталог скрипта, PYTHONPATH, stdlib, site-packages
  • перемагає перший збіг, тож локальний файл може затінити стандартний модуль

Практичний приклад

Створення власного скрипта з іменем `json.py` в кореневому каталозі проєкту може тихо зламати кожен наступний `import json` у цьому ж каталозі, бо локальний файл знаходиться в `sys.path` раніше за модуль `json` стандартної бібліотеки.

#imports#modules

Джерела

The import systemPython Software Foundation · Module resolution, sys.path, packages and import caching
Modules, packaging and environmentsMiddleCommonTheory

Яка мета файлу `__init__.py`, і чи все ще потрібен він, щоб перетворити каталог на пакет?

Відповідь

`__init__.py` позначає каталог як звичайний пакет і автоматично виконується при першому імпорті будь-якого модуля всередині цього пакета, що робить його природним місцем для визначення того, що пакет виставляє на верхньому рівні, виконання налаштування на рівні всього пакета чи явного контролю `from package import *` через `__all__`. З Python 3.3 (PEP 420) каталог без `__init__.py` все ще можна імпортувати як неявний «простір-імен пакет» (namespace package), що корисно для розділення однієї логічної пакетної структури на кілька розподілених каталогів, але явний `__init__.py` залишається звичайним, більш передбачуваним вибором для звичайного пакета застосунку.

Сильна відповідь включає

  • __init__.py виконується при першому імпорті чогось усередині пакета
  • природне місце для верхньорівневих експортів, налаштування чи __all__
  • простір-імен пакети (PEP 420, 3.3+) дозволяють опустити його, але явний варіант типовий

Практичний приклад

`mypackage/__init__.py` з вмістом `from .core import main_function` дозволяє викликачам писати `from mypackage import main_function` замість глибшого `from mypackage.core import main_function`.

#packages#modules

Джерела

The import systemPython Software Foundation · Module resolution, sys.path, packages and import cachingThe Python TutorialPython Software Foundation · Baseline syntax, control flow, functions and module basics
Modules, packaging and environmentsJuniorVery commonPractical

Чому кожен проєкт Python потребує власного віртуального середовища, а не глобального встановлення пакетів?

Відповідь

Віртуальне середовище, створене через `python -m venv .venv`, надає проєкту власний ізольований інтерпретатор Python і каталог `site-packages`, тож його залежності та їхні точні версії не конфліктують із залежностями іншого проєкту чи з пакетами, встановленими глобально в системі. Без ізоляції два проєкти, яким потрібні несумісні версії однієї бібліотеки, не можуть обидва працювати на одній машині, а оновлення залежності для одного проєкту ризикує тихо зламати непов'язаний проєкт, що випадково поділяв те саме глобальне середовище.

Сильна відповідь включає

  • venv створює ізольований інтерпретатор і site-packages для проєкту
  • уникає конфліктів версій між непов'язаними проєктами
  • без ізоляції оновлення спільної глобальної залежності може зламати інші проєкти

Практичний приклад

Проєкту A потрібен `requests==2.20`, а проєкту B — `requests==2.32`; без окремих віртуальних середовищ встановлення однієї версії глобально ламає той проєкт, якому була потрібна інша.

#venv#environments

Джерела

venv — Creation of virtual environmentsPython Software Foundation · Virtual environment isolation and interpreter/site-packages layoutPython Packaging User GuidePython Packaging Authority · Project metadata, wheels, dependency and environment management practices
Modules, packaging and environmentsMiddleCommonPractical

Яка різниця між слабко зафіксованим `requirements.txt` і повністю заблокованим файлом залежностей, і чому важлива відтворюваність?

Відповідь

Слабко зафіксований файл вимог (`requests>=2.20`) називає лише прямі залежності й залишає точні розв'язані версії, а також версію кожної транзитивної залежності, на розсуд того, що `pip` розв'яже на момент встановлення, що може відрізнятися між двома встановленнями, зробленими з різницею в тижні, коли публікуються нові версії. Повністю заблокований файл фіксує кожен пакет, прямий і транзитивний, до точної версії (і часто хешу вмісту), що гарантує послідовне встановлення тих самих задекларованих артефактів у межах середовищ, для яких цей файл блокування справді був побудований — конкретна версія Python, ОС і архітектура, з доступними заблокованими артефактами джерела чи wheel. Сам по собі він не гарантує ідентичного результату на будь-якій машині чи назавжди в майбутньому: специфічні для платформи wheel, маркери середовища й артефакти, що пізніше стають недоступними в індексі, все ще можуть давати різні результати між справді різними середовищами чи з часом. `pylock.toml` (PEP 751) — поточний стандартизований, незалежний від інструменту формат файлу блокування, створений саме для відтворюваних встановлень у межах задекларованого набору сумісних середовищ. Відтворюваність у цих межах все ще необхідна для стабільних збірок, налагодження проблем «у мене працює» й безпечних відкатів.

Сильна відповідь включає

  • слабка фіксація залишає точні й транзитивні версії на розсуд розв'язання при встановленні
  • файл блокування фіксує кожен пакет до точної версії/хешу, відтворюваний у межах задекларованих сумісних середовищ — не безумовна гарантія назавжди й будь-де
  • pylock.toml (PEP 751) — поточний стандартизований формат файлу блокування для цього

Практичний приклад

Конвеєр CI, що встановлює зі слабко зафіксованого `requirements.txt`, може почати падати за ніч, коли непов'язана транзитивна залежність випустить зворотно несумісну зміну, тоді як заблокований файл тримає збірки стабільними, доки хтось навмисно його не оновить.

#packaging#dependencies

Джерела

Python Packaging User GuidePython Packaging Authority · Project metadata, wheels, dependency and environment management practices
Modules, packaging and environmentsMiddleCommonTheory

Що таке `pyproject.toml`, і що він замінив у сучасному пакуванні Python?

Відповідь

`pyproject.toml` — стандартизований, незалежний від інструменту файл конфігурації (PEP 518 для таблиці системи збирання, PEP 621 для метаданих проєкту), який проєкт Python використовує, щоб оголосити свій бекенд збирання, залежності, ім'я, версію та інші метадані в одному місці. Це сучасна стандартна поверхня для цього, що переважно замінює старіший підхід із написаним вручну скриптом `setup.py` плюс розрізненими секціями `setup.cfg` як основне місце для метаданих проєкту — але самі `setup.py` й Setuptools не застарілі; PyPA спеціально позначила застарілим саме пряме виконання `setup.py` як команди (`python setup.py install`, `python setup.py sdist`), на користь стандартизованих інструментів-фронтендів збирання, що читають `pyproject.toml`. Сучасні інструменти на кшталт лінтерів, форматерів і тестових раннерів також дедалі частіше читають власну конфігурацію з `pyproject.toml`, а стандартизована таблиця `[dependency-groups]` (PEP 735) тепер дозволяє проєкту оголошувати групи залежностей лише для розробки — тести, лінтинг, документацію — теж там.

Сильна відповідь включає

  • стандартизований файл конфігурації для системи збирання й метаданих (PEP 518/621)
  • setup.py й Setuptools не застарілі — застаріле лише пряме виконання setup.py як команди
  • [dependency-groups] (PEP 735) тепер дозволяє оголошувати dev-залежності і в pyproject.toml

Практичний приклад

Єдиний `pyproject.toml` може оголошувати бекенд збирання в `[build-system]`, метадані пакета й залежності в `[project]`, а конфігурацію pytest чи ruff — у `[tool.pytest.ini_options]` чи `[tool.ruff]`, усе в одному файлі.

#packaging#tooling

Джерела

Python Packaging User GuidePython Packaging Authority · Project metadata, wheels, dependency and environment management practices
Modules, packaging and environmentsSeniorOccasionalTheory

Яка різниця між дистрибутивом вихідного коду (sdist) і wheel, і чому wheel встановлюються швидше?

Відповідь

Дистрибутив вихідного коду (`.tar.gz`) містить сирий вихідний код пакета плюс інструкції збирання, і його встановлення може вимагати запуску кроку збирання, включно з компіляцією будь-яких C-розширень, на машині, що встановлює, у момент встановлення. Wheel (`.whl`) — це попередньо зібраний, готовий до встановлення архів — для чисто-Python пакета його достатньо розпакувати в `site-packages`, а для пакета зі скомпільованими розширеннями специфічний для платформи wheel уже містить скомпільований бінарний файл для цієї ОС і версії Python, тож `pip` ніколи не потребує робочого компілятора C на машині, що встановлює, для успіху.

Сильна відповідь включає

  • sdist постачає джерело плюс інструкції збирання, може компілювати при встановленні
  • wheel попередньо зібраний і готовий до встановлення, часто просто розпакування
  • специфічні для платформи wheel уникають потреби в компіляторі на машині встановлення

Практичний приклад

Встановлення wheel для NumPy займає секунди, бо завантажується скомпільований бінарний файл, що відповідає вашій ОС і версії Python, тоді як збирання NumPy з його sdist на машині без потрібного набору інструментів компіляції може взагалі провалитися.

#packaging#wheel

Джерела

Python Packaging User GuidePython Packaging Authority · Project metadata, wheels, dependency and environment management practices
Modules, packaging and environmentsMiddleCommonPractical

Яка різниця між абсолютним і відносним імпортом, і коли відносний імпорт зазнає невдачі?

Відповідь

Абсолютний імпорт (`from mypackage.utils import helper`) прописує повний шлях від пакета верхнього рівня і працює незалежно від того, який модуль виконує імпорт. Відносний імпорт (`from .utils import helper` чи `from ..other import thing`) виражається відносно позиції модуля, що імпортує, всередині власного пакета, використовуючи провідні крапки для позначення «цей пакет» чи «батьківський пакет». Відносні імпорти працюють лише всередині справжнього пакета (модуля, для якого Python знає `__package__`); запуск скрипта напряму через `python module.py` замість `python -m package.module` встановлює `__package__` у порожній контекст, і будь-який відносний імпорт усередині цього скрипта тоді зазнає невдачі з `ImportError`.

Сильна відповідь включає

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

Практичний приклад

Запуск `python mypackage/module.py` напряму призводить до того, що будь-який `from . import sibling` усередині `module.py` викликає `ImportError: attempted relative import with no known parent package`, тоді як `python -m mypackage.module` працює коректно.

#imports#packages#gotchas

Джерела

The import systemPython Software Foundation · Module resolution, sys.path, packages and import caching
Modules, packaging and environmentsSeniorOccasionalPractical

Що робить `pip install -e .` (редаговане встановлення), і чому це корисно під час локальної розробки?

Відповідь

Редаговане встановлення реєструє пакет у поточному середовищі, не копіюючи його файли в `site-packages`; натомість воно виставляє вихідний каталог проєкту середовищу через механізм, визначений бекендом (PEP 660, зазвичай невеликий файндер чи хук шляху, а не буквальне символічне посилання), тож звичайна зміна вихідного коду Python зазвичай набуває чинності наступного разу, коли пакет імпортують, без кроку перевстановлення. Це стандартний робочий процес для розробки бібліотеки поряд із застосунком, що від неї залежить. Точна механіка визначається бекендом, а не є єдиною універсальною гарантією: додавання нового модуля, зміна метаданих часу збирання на кшталт точок входу консольних скриптів чи торкання чогось, що вимагає перезбирання скомпільованого розширення, все ще можуть вимагати повторного запуску редагованого встановлення, тож «кожна зміна набуває чинності миттєво» — корисне наближення, а не абсолютне правило.

Сильна відповідь включає

  • виставляє вихідний каталог через механізм бекенду (PEP 660), не обов'язково буквальне символічне посилання
  • звичайні зміни вихідного коду зазвичай набувають чинності без перевстановлення
  • нові модулі, зміни метаданих чи перезбирання розширень все ще можуть вимагати повторного встановлення

Практичний приклад

`pip install -e ./mylib` усередині віртуального середовища застосунку дозволяє розробнику редагувати вихідний код `mylib` напряму й бачити зміну відображеною в застосунку, що працює, вже при наступному імпорті, без перевстановлення.

#packaging#development-workflow

Джерела

Python Packaging User GuidePython Packaging Authority · Project metadata, wheels, dependency and environment management practices
Typing and static analysisJuniorVery commonTheory

Який базовий синтаксис анотацій типів для змінних, параметрів функцій і значень, що повертаються?

Відповідь

Анотація змінної пишеться як `name: Type = value`, анотація параметра — `def f(param: Type):`, а анотація типу, що повертається, — `def f() -> Type:`. Python ніколи не примушує, не приводить і не перевіряє їх під час виклику — але вони не суто зручність для статичних перевіряльників: анотації також є реальними метаданими часу виконання, доступними через `__annotations__` чи модуль `annotationlib`, які фреймворки й бібліотеки справді читають, щоб керувати власною поведінкою, наприклад валідувати вхідні дані запиту чи будувати CLI за сигнатурою функції. З Python 3.14 (PEP 649/749) анотації обчислюються ліниво за замовчуванням, а не одразу в момент визначення. Для більшості розробників застосунків найбільша повсякденна цінність все ще йде від статичних перевіряльників типів, що читають анотації заздалегідь, і від IDE, що використовують їх для автодоповнення й підсвічування помилок, тому команди впроваджують анотації типів переважно для виявлення невідповідностей до запуску коду.

Сильна відповідь включає

  • синтаксис анотацій змінної, параметра й -> для значення, що повертається
  • ніколи не примушується при виклику, але є й реальними метаданими часу виконання — не суто зручність для статичних перевіряльників
  • з 3.14 анотації обчислюються ліниво за замовчуванням (PEP 649/749)

Практичний приклад

`def total_price(items: list[str], tax_rate: float = 0.2) -> float:` достатньо чітко документує намір, щоб перевіряльник типів позначив `total_price(["a"], "20%")` як помилку ще до запуску коду.

#typing#type-hints

Джерела

typing — Support for type hintsPython Software Foundation · Generics, protocols, overloads and typing-module semanticsPEP 484 — Type HintsPython Software Foundation · Foundational type-hint syntax and static-typing intent
Typing and static analysisSeniorOccasionalTheory

Яку проблему вирішує узагальнена (generic) функція чи клас з `TypeVar` (чи новішим синтаксисом `class Stack[T]:`)?

Відповідь

Узагальнення дозволяє написати функцію чи клас один раз, зберігаючи при цьому точний зв'язок між типами вхідних і вихідних даних для перевіряльника типів, замість того щоб вдаватися до непридатного `Any`, який повністю відкидає інформацію про тип. `def first(items: list[T]) -> T:` каже перевіряльнику, що яким би не був конкретний тип, що заповнює список, значення, що повертається, — той самий тип: передавши `list[int]`, отримуєш назад `int`, передавши `list[str]` — `str`, що `Any` ніколи не міг би виразити. Синтаксис PEP 695 з Python 3.12, `def first[T](items: list[T]) -> T:`, виражає ту саму ідею без окремого оголошення `TypeVar`.

Сильна відповідь включає

  • зберігає точний зв'язок типів вхід-вихід замість Any
  • та сама змінна типу пов'язує кілька позицій між викликами
  • синтаксис PEP 695 (3.12) виражає те саме без окремого TypeVar

Практичний приклад

`class Stack[T]:\n def push(self, item: T) -> None: ...\n def pop(self) -> T: ...` гарантує, що перевіряльник позначить `Stack[int]().push("oops")` як помилку — те, що нетипізований чи типізований як `Any` стек ніколи не зміг би виявити.

#typing#generics

Джерела

typing — Support for type hintsPython Software Foundation · Generics, protocols, overloads and typing-module semanticsWhat's new in Python 3.14Python Software Foundation · Current-version language and runtime changes, including template strings and free-threading support
Typing and static analysisMiddleCommonPractical

Яка різниця між `Optional[X]` і `X | None`, і чому синтаксис об'єднання через `|` тепер надається перевага?

Відповідь

`Optional[X]` точно еквівалентний `Union[X, None]`: означає, що значення — або типу `X`, або `None`. З PEP 604 у Python 3.10 `X | None` (і загальніше `X | Y` для будь-якого об'єднання) виражає те саме через синтаксис вбудованого оператора, що взагалі не потребує імпорту з `typing`, читається природніше й працює прямо зі звичайними вбудованими типами на кшталт `list[int] | None`, замість старішого, багатослівнішого `Optional[List[int]]`, що вимагав імпорту `List` з `typing` на версіях Python до 3.9.

Сильна відповідь включає

  • Optional[X] точно дорівнює Union[X, None]
  • синтаксис X | None з PEP 604 (3.10) не потребує імпорту typing
  • читається природніше й компонується зі звичайними вбудованими generic

Практичний приклад

`def find_user(id: int) -> User | None:` — сучасний еквівалент `def find_user(id: int) -> Optional[User]:`, обидва кажуть викликачу, що функція може повернути `None`.

#typing#optional#syntax

Джерела

typing — Support for type hintsPython Software Foundation · Generics, protocols, overloads and typing-module semanticsWhat's new in Python 3.14Python Software Foundation · Current-version language and runtime changes, including template strings and free-threading support
Typing and static analysisSeniorSpecialistPractical

Що дозволяє виразити `@typing.overload`, чого не може одна сигнатура функції з анотаціями типів?

Відповідь

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

Сильна відповідь включає

  • оголошує кілька сигнатур для тієї самої назви функції для вибору перевіряльником
  • за ними йде одна реальна реалізація без декоратора, що справді виконується
  • потрібно, коли тип, що повертається, справді залежить від переданих аргументів

Практичний приклад

`@overload\ndef parse(value: str) -> dict: ...\n@overload\ndef parse(value: bytes) -> bytes: ...\ndef parse(value): ...` дає перевіряльнику знати, що `parse("x")` повертає `dict`, а `parse(b"x")` — `bytes`.

#typing#overload

Джерела

typing — Support for type hintsPython Software Foundation · Generics, protocols, overloads and typing-module semantics
Typing and static analysisMiddleCommonTooling

Що насправді роблять інструменти на кшталт mypy і pyright, і чому сам інтерпретатор Python не виконує цю перевірку?

Відповідь

mypy і pyright — окремі інструменти статичного аналізу, що читають анотації типів кодової бази та реальні шляхи коду, нічого не виконуючи, а потім звітують неузгодженості, які можуть виявити в межах своєї аналізованої моделі типів і налаштованої суворості — передача `str` там, де очікується `int`, виклик методу, якого не існує для даного типу, чи повернення неправильного типу з функції. Вони не виявляють кожну невідповідність: динамічні можливості, код, типізований як `Any` чи взагалі без анотацій, неповні сторонні заглушки типів і код, який вони не можуть статично проаналізувати (частина метапрограмування чи рефлексії) можуть приховати реальні невідповідності навіть при суворій конфігурації. Сам інтерпретатор не виконує цю перевірку, бо система типізації Python, за власною специфікацією, навмисно є опційними, поступовими метаданими статичного аналізу, а не контрактом часу виконання — це дизайнерське рішення про повну сумісність типізованого й нетипізованого коду, а не щось, що зводиться до однієї причини продуктивності.

Сильна відповідь включає

  • звітують неузгодженості в межах аналізованої моделі типів і налаштованої суворості, не вичерпно кожну невідповідність
  • динамічний код, Any й неповна типізація можуть приховати реальні помилки навіть при суворих перевіряльниках
  • система типізації Python навмисно опційна, поступова, метадані за специфікацією, не через одну причину продуктивності

Практичний приклад

`mypy app.py` може виявити `user.name.append("x")` (виклик методу списку на атрибуті `str`) як помилку типу ще до запуску коду, тоді як сам інтерпретатор виявив би цю помилку лише якщо й коли цей конкретний рядок виконається.

#typing#tooling#mypy

Джерела

typing — Support for type hintsPython Software Foundation · Generics, protocols, overloads and typing-module semanticsPEP 484 — Type HintsPython Software Foundation · Foundational type-hint syntax and static-typing intent
Typing and static analysisSeniorOccasionalPractical

Що додає `typing.TypedDict` для коду, що працює зі звичайними словниками фіксованої, відомої форми, як-от розібраний JSON?

Відповідь

`TypedDict` дозволяє оголосити, що звичайний `dict` має мати конкретний, фіксований набір рядкових ключів з конкретними типами значень — наприклад, для JSON-корисних навантажень із зовнішнього API, тоді як значення під час виконання залишається звичайним об'єктом `dict` без жодних накладних витрат, без нового типу під час виконання й без валідації під час виконання. Статичний перевіряльник типів тоді позначає звернення до неоголошеного ключа чи присвоєння неправильного типу оголошеному ключу, виявляючи цілий клас помилок одруківок і невідповідностей форми в коді, що інакше просто використовував би довільні, нетипізовані словники всюди.

Сильна відповідь включає

  • оголошує фіксований набір рядкових ключів і типів значень для форми dict
  • значення під час виконання все ще звичайний dict, без накладних витрат, без валідації
  • перевіряльник позначає неоголошені ключі й невідповідності типів при присвоєнні

Практичний приклад

`class UserPayload(TypedDict):\n id: int\n name: str` дозволяє перевіряльнику позначити `payload["nam"]` як невідомий ключ чи `payload["id"] = "5"` як невідповідність типу — обидві поширені помилки в коді, що працює з сирими словниками.

#typing#typeddict

Джерела

typing — Support for type hintsPython Software Foundation · Generics, protocols, overloads and typing-module semantics
Typing and static analysisSeniorOccasionalTheory

Яка різниця між анотуванням чогось як `Any` проти `object`, хоча обидва приймають будь-яке значення?

Відповідь

Обидва приймають значення буквально будь-якого типу, але поводяться протилежно для перевіряльника типів надалі. `object` — справжній корінь ієрархії типів Python, тож перевіряльник дозволяє викликати лише ті методи, які справді підтримує кожен об'єкт (як `__repr__`), а використання його в конкретнішому контексті спершу вимагає явної перевірки звуження, наприклад `isinstance`. `Any` — це «люк для втечі» перевіряльника, що повністю вимикає перевірку типів для цього значення: можна викликати будь-який метод, передавати його будь-куди, і перевіряльник не поскаржиться, навіть якщо ця операція провалилася б під час виконання, що робить `Any` набагато небезпечнішим і чимось, що варто використовувати рідко, зазвичай лише на справжніх межах динамічних даних.

Сильна відповідь включає

  • object — справжній корінь ієрархії типів, дозволені лише універсальні операції
  • Any повністю вимикає перевірку типів для цього значення
  • Any варто використовувати рідко, лише на справжніх межах динамічних даних

Практичний приклад

Перевіряльник позначає `def f(x: object): x.upper()` як помилку, бо не кожен `object` має `.upper()`, але мовчки приймає `def f(x: Any): x.upper()`, хоча передача `int` під час виконання призвела б до аварійного завершення.

#typing#any#object

Джерела

typing — Support for type hintsPython Software Foundation · Generics, protocols, overloads and typing-module semanticsPEP 484 — Type HintsPython Software Foundation · Foundational type-hint syntax and static-typing intent
Typing and static analysisMiddleCommonPractical

Що таке звуження типу (type narrowing), і як перевірка `isinstance()` уможливлює його для статичного перевіряльника типів?

Відповідь

Звуження типу — це коли перевіряльник типів уточнює відомий тип змінної в конкретній гілці коду на основі перевірки під час виконання, яку виконує код, без потреби додавати жодної додаткової анотації типу. Усередині `if isinstance(value, str):` перевіряльник трактує `value` як точно `str` для решти цієї гілки, навіть якщо оголошений тип був ширшим об'єднанням на кшталт `str | int`, дозволяючи безпечно викликати `.upper()` усередині гілки, тоді як перевіряльник все ще позначає `.upper()` поза нею, бо там `value` все ще міг би бути `int`.

Сильна відповідь включає

  • перевіряльник уточнює відомий тип змінної в гілці на основі перевірки під час виконання
  • isinstance — найпоширеніша перевірка звуження, без додаткової анотації
  • звуження застосовується лише всередині гілки, де перевірка виконалась

Практичний приклад

`def describe(value: str | int) -> str:\n if isinstance(value, str):\n return value.upper()\n return str(value * 2)` чисто проходить перевірку типів, бо кожна гілка звужує `value` точно до того типу, який використовує.

#typing#isinstance

Джерела

typing — Support for type hintsPython Software Foundation · Generics, protocols, overloads and typing-module semantics
Standard library and toolingMiddleCommonPractical

Які проблеми вирішують `collections.defaultdict`, `Counter` і `deque` порівняно зі звичайним `dict` чи `list`?

Відповідь

`defaultdict(list)` усуває потребу перевіряти наявність ключа перед додаванням до нього, бо звернення до відсутнього ключа автоматично створює його за допомогою наданої функції-фабрики замість виклику `KeyError`. `Counter` — спеціалізований словник для підрахунку хешованих елементів, що безкоштовно дає `.most_common(n)` та арифметику між лічильниками замість написання ручних циклів підрахунку. `deque` (двостороння черга) дає O(1) для додавання й вилучення з *обох* кінців, на відміну від звичайного `list`, чиї `insert(0, x)` чи `pop(0)` мають складність O(n), бо кожен елемент, що залишився, доводиться зсувати.

Сильна відповідь включає

  • defaultdict автоматично створює відсутні ключі через фабрику
  • Counter підраховує хешовані елементи з most_common() та арифметикою
  • deque дає O(1) на обох кінцях, на відміну від O(n) list на початку

Практичний приклад

`groups = defaultdict(list)\nfor item in items:\n groups[item.category].append(item)` групує елементи за категорією у трьох рядках, без шаблонного `if category not in groups:`.

#collections#data-structures

Джерела

collections — Container datatypesPython Software Foundation · deque, defaultdict, Counter, namedtuple and ChainMap semantics
Standard library and toolingMiddleOccasionalTheory

Як `collections.namedtuple` порівнюється з `@dataclass` для представлення невеликого, фіксованого запису?

Відповідь

`namedtuple` створює підклас кортежу з іменованими полями, тож екземпляри залишаються незмінюваними й поводяться як звичайний кортеж у всьому, що має значення — розпаковувані, індексовані за позицією — водночас дозволяючи доступ до атрибутів за іменем. Але, як і будь-який кортеж, екземпляр `namedtuple` хешований лише якщо кожне його поле саме хешоване; поле namedtuple, що містить `list`, наприклад, робить нехешованим і сам екземпляр, тож хешованість — не безумовна властивість цього типу. `@dataclass` створює звичайний клас, за замовчуванням змінюваний (чи незмінюваний, якщо передати `frozen=True`), підтримує методи, спадковість і не-кортежне внутрішнє зберігання, і природніше інтегрується з перевіряльниками типів та IDE. Обирайте `namedtuple` для легкого, сумісного з кортежем значення, що має поводитися точно як кортеж у наявному коді; обирайте `dataclass` для всього, що потребує змінюваності, власних методів чи багатшої підтримки типізації.

Сильна відповідь включає

  • namedtuple — справжній підклас кортежу, незмінюваний і позиційно розпаковуваний
  • хешованість namedtuple, як і будь-якого кортежу, залежить від хешованості кожного поля
  • dataclass — звичайний клас, за замовчуванням змінюваний, підтримує методи й спадковість

Практичний приклад

`Point = namedtuple("Point", ["x", "y"])` все ще підтримує розпаковку `x, y = point` і `point[0]`, чого версія `@dataclass` не підтримувала б без ручного додавання `__iter__` і `__getitem__`.

#collections#namedtuple#dataclasses

Джерела

collections — Container datatypesPython Software Foundation · deque, defaultdict, Counter, namedtuple and ChainMap semanticsdataclasses — Data ClassesPython Software Foundation · Generated dunder methods, field defaults and frozen/eq/order semantics
Standard library and toolingJuniorCommonPractical

Чому `pathlib.Path` зазвичай рекомендують замість маніпуляції рядками через `os.path` для шляхів файлової системи?

Відповідь

`os.path` представляє шлях як звичайний рядок і надає вільні функції на кшталт `os.path.join()` та `os.path.splitext()`, що працюють над ним, що функціонує, але розкидає логіку роботи зі шляхами по непов'язаних викликах функцій і вимагає уваги до різниці роздільників між операційними системами. `pathlib.Path` представляє шлях як об'єкт з методами та перевантаженим оператором `/` для з'єднання (`base_dir / "sub" / "file.txt"`), прозоро обробляє відмінності роздільників Windows і POSIX, і об'єднує пов'язані операції на кшталт `.exists()`, `.read_text()`, `.glob()` та `.parent` прямо в об'єкті замість окремих викликів функцій рівня модуля.

Сильна відповідь включає

  • os.path трактує шлях як звичайний рядок з вільними функціями над ним
  • pathlib.Path — об'єкт з перевантаженим оператором / для з'єднання
  • прозоро обробляє роздільники ОС, об'єднує операції як методи

Практичний приклад

`Path("data") / "reports" / f"{year}.csv"` будує коректний шлях і на Windows, і на Linux, ніколи не пишучи літерального роздільника `/` чи `\\`.

#pathlib#filesystem

Джерела

pathlib — Object-oriented filesystem pathsPython Software Foundation · Cross-platform path handling that replaces most os.path usageTop Python Interview Questions and AnswersDataCamp · Coverage signal for data-oriented and standard-library questions
Standard library and toolingMiddleVery commonPractical

Чому модуль `logging` надається перевага над розкиданими операторами `print()` для діагностичного виводу в реальних застосунках?

Відповідь

`logging` надає кожному повідомленню рівень серйозності (`DEBUG`, `INFO`, `WARNING`, `ERROR`, `CRITICAL`), тож детальність можна налаштовувати для кожного середовища, не чіпаючи вихідний код, спрямовує повідомлення незалежно в кілька призначень (консоль, файли, що ротуються, віддалені агрегатори логів) через конфігуровані обробники, і автоматично включає структурований контекст на кшталт міток часу, імені логера й модуля. `print()` не має нічого з цієї структури «з коробки»: за замовчуванням він пише в stdout без рівнів серйозності, маршрутизації обробників чи автоматичного контексту, і хоча `print(..., file=sys.stderr)` може перенаправити окремий виклик, а оболонка може перенаправити stdout повністю, жоден із варіантів не дає незалежно конфігурованих рівнів і призначень для кожного повідомлення, як у `logging`. Саме цей розрив у структурі — а не просто відсутність будь-якого перенаправлення — причина, чому оператори `print()` зазвичай залишаються як сміття для налагодження, яке доводиться вручну знаходити й видаляти, тоді як виклики `logging` лишаються корисними в продакшені.

Сильна відповідь включає

  • рівні серйозності дозволяють налаштовувати детальність без зміни коду
  • обробники незалежно спрямовують повідомлення в кілька призначень
  • print() можна перенаправити, але йому все ще бракує рівнів, маршрутизації обробників і контексту logging

Практичний приклад

`logging.getLogger(__name__).warning("retrying %s after %s", url, error)` можна приглушити в продакшені, піднявши рівень кореневого логера до `ERROR`, не видаляючи й не коментуючи жодного рядка коду.

#logging#diagnostics

Джерела

logging — Logging facility for PythonPython Software Foundation · Loggers, handlers, levels and structured diagnostic output
Pytest & fixturesMiddleVery commonPractical

Що таке фікстура pytest, і як її аргумент `scope` контролює, як часто вона виконується?

Відповідь

Фікстура pytest — це функція, декорована `@pytest.fixture`, що надає налаштування (і, після `yield`, очищення) для тестів, які оголошують її як параметр за іменем; pytest розв'язує та впроваджує її автоматично без явного виклику. Аргумент `scope` (`function` за замовчуванням, `class`, `module`, `package` чи `session`) контролює, скільки разів фікстура реально виконується проти того, скільки тестів ділять той самий екземпляр: фікстура з областю `session` виконується один раз для всього тестового прогону й ділиться кожним тестом, що її запитує, що необхідно для дорогого налаштування на кшталт підключення до бази даних, тоді як область `function` заново запускає налаштування й очищення для кожного окремого тесту, щоб гарантувати ізоляцію.

Сильна відповідь включає

  • декорована функція, що надає налаштування, опційно очищення після yield, впроваджується за іменем параметра
  • scope контролює, скільки тестів ділять один екземпляр фікстури
  • session для дорогого спільного налаштування, function для повної ізоляції

Практичний приклад

`@pytest.fixture(scope="session")\ndef db_connection():\n conn = connect()\n yield conn\n conn.close()` відкриває одне підключення до бази даних для всього набору тестів замість повторного підключення перед кожним окремим тестом.

#pytest#testing

Джерела

pytest documentationpytest · Fixture scope, parametrization, markers and plugin architectureunittest — Unit testing frameworkPython Software Foundation · Baseline test-case, mock and assertion semantics shared across Python test tooling
Pytest & fixturesMiddleCommonPractical

Що робить `@pytest.mark.parametrize`, і чому це зазвичай краще за цикл `for` над тестовими випадками всередині однієї тестової функції?

Відповідь

`@pytest.mark.parametrize("input,expected", [(1, 2), (2, 4)])` запускає декоровану тестову функцію по одному разу для кожного кортежу аргументів, і pytest звітує кожен як окремий, незалежно названий, незалежно пройдений/провалений тестовий результат. Цикл `for` усередині однієї тестової функції натомість виконує всі випадки в межах одного тесту, тож перший провальний випадок зупиняє цикл і приховує результати всіх наступних, а провал звітує лише загальну назву охоплюючого тесту замість точного визначення, який саме вхід провалився.

Сильна відповідь включає

  • запускає тест по разу для кожного кортежу параметрів, кожен як окремий результат
  • цикл for зупиняється на першому провалі й приховує наступні випадки
  • parametrize точно вказує, який вхід провалився, замість одного загального результату

Практичний приклад

`@pytest.mark.parametrize("n,expected", [(0, 0), (1, 1), (5, 120)])\ndef test_factorial(n, expected):\n assert factorial(n) == expected` звітує `test_factorial[5-120]` як окремо провалений тест, якщо ламається лише цей випадок.

#pytest#testing

Джерела

pytest documentationpytest · Fixture scope, parametrization, markers and plugin architecture
Mocking & test isolationSeniorCommonPractical

Що робить `unittest.mock.patch`, і чому потрібно патчити ім'я там, де воно шукається, а не там, де воно визначено?

Відповідь

`patch` тимчасово замінює атрибут (часто функцію, клас чи об'єкт) на `Mock` на час тесту й автоматично відновлює оригінал після, навіть якщо тест провалюється. Критичне, часто незрозуміле правило — потрібно патчити ім'я так, як воно імпортоване в модуль, що тестується, а не там, де живе оригінальний об'єкт, бо `from module_a import func; func()` усередині `module_b` створює окрему прив'язку до `func` у власному просторі імен `module_b` — патчинг `module_a.func` після того, як цей імпорт уже відбувся, взагалі не торкається вже прив'язаного посилання `module_b`.

Сильна відповідь включає

  • тимчасово замінює атрибут на Mock, автоматично відновлює після тесту
  • патчити ім'я так, як воно шукається в модулі, що тестується, а не де визначено
  • уже завершений імпорт створює окрему прив'язку в просторі імен модуля, що імпортує

Практичний приклад

Якщо `service.py` робить `from utils import fetch_data`, тест має патчити `service.fetch_data`, а не `utils.fetch_data`, бо простір імен `service` вже тримає власне посилання на оригінальну `fetch_data`.

#testing#mocking#gotchas

Джерела

unittest — Unit testing frameworkPython Software Foundation · Baseline test-case, mock and assertion semantics shared across Python test toolingpytest documentationpytest · Fixture scope, parametrization, markers and plugin architecture
Standard library and toolingJuniorCommonPractical

Як вбудована функція `breakpoint()` допомагає інтерактивно налагоджувати код Python, і що вона робить за замовчуванням?

Відповідь

Виклик `breakpoint()` будь-де в коді (введений PEP 553 у Python 3.7) призупиняє виконання саме на цьому рядку й переходить у `pdb`, стандартний інтерактивний дебагер бібліотеки, дозволяючи інспектувати локальні змінні, обчислювати вирази й покроково проходити наступні рядки з підказки саме там, де програма зупинилася. Вона враховує змінну середовища `PYTHONBREAKPOINT`, тож команди можуть глобально перенаправити її на інший дебагер (як `ipdb` чи віддалений дебагер) чи повністю вимкнути в продакшені (`PYTHONBREAKPOINT=0`), не редагуючи жодного рядка вихідного коду.

Сильна відповідь включає

  • призупиняє виконання на цьому рядку й переходить у pdb за замовчуванням
  • дозволяє інспектувати локальні змінні, обчислювати вирази й проходити код покроково
  • змінна PYTHONBREAKPOINT дозволяє перенаправити чи вимкнути без зміни коду

Практичний приклад

Додавання `breakpoint()` прямо перед проблемним обчисленням переходить в інтерактивну підказку `(Pdb)` саме в цій точці, де введення `p variable_name` показує її поточне значення без додавання жодних тимчасових `print()`.

#debugging#pdb

Джерела

The Python Standard LibraryPython Software Foundation · Authoritative behavior for built-in types and standard library modules
Standard library and toolingMiddleOccasionalPractical

Що робить `functools.partial`, і чому йому надають перевагу над маленькою обгорткою `lambda`?

Відповідь

`functools.partial(func, *args, **kwargs)` повертає нову виклику сутність, яка при подальшому виклику викликає `func` з уже заповненими попередньо переданими аргументами плюс будь-якими додатковими аргументами, наданими під час виклику. На відміну від обгортки `lambda x: func(fixed_arg, x)`, об'єкт `partial` зберігає доступні для інтроспекції посилання на оригінальну функцію та її прив'язані аргументи (`.func`, `.args`, `.keywords`) і передає намір — «це `func` з уже зафіксованими деякими аргументами» — пряміше, ніж довільна функція-обгортка. Він також добре поєднується з `pickle` в контекстах multiprocessing, де локальна lambda провалилася б одразу, за умови що сама обгорнута виклична сутність і прив'язані аргументи придатні для pickle — `partial`, що обгортає непридатну для pickle виклику сутність чи аргумент, не стає придатним для pickle просто через обгортання.

Сильна відповідь включає

  • повертає виклику сутність з попередньо заповненими аргументами, решта — під час виклику
  • зберігає доступні для інтроспекції .func/.args/.keywords, на відміну від непрозорої lambda
  • придатність для pickle в multiprocessing залежить від того, чи самі обгорнута функція й аргументи придатні для pickle

Практичний приклад

`double = functools.partial(multiply, 2)` поводиться точно як `double(5) == 10`, а `pool.map(functools.partial(process, config), items)` безпечно працює через межі процесів там, де `lambda x: process(config, x)` викликала б помилку pickle.

#functools#functional-programming

Джерела

functools — Higher-order functions and operations on callable objectsPython Software Foundation · lru_cache, partial, wraps, reduce and singledispatch behaviormultiprocessing — Process-based parallelismPython Software Foundation · Process pools, pickling constraints and inter-process communication
Standard library and toolingMiddleCommonTroubleshooting

Яка різниця між наївним (naive) і обізнаним (aware) об'єктом `datetime`, і чому їх змішування викликає `TypeError`?

Відповідь

Наївний `datetime` не має жодної прикріпленої інформації про часовий пояс — це просто точка в календарі без способу дізнатися, який часовий пояс вона представляє, тоді як обізнаний `datetime` несе об'єкт `tzinfo`, що прив'язує його до конкретного зміщення від UTC. Порівняння наївного й обізнаного datetime на рівність чи нерівність (`==`, `!=`) не викликає помилку: Python просто вважає їх нерівними, бо не має підстав вважати їх тією самою миттю. Порівняння порядку (`<`, `>`, `<=`, `>=`) і віднімання справді викликають `TypeError: can't compare offset-naive and offset-aware datetimes`, бо справді немає правильної відповіді на те, що мало б означати «раніше» чи «наскільки далеко» без знання неявного часового поясу наївного значення, а тихе вгадування спричинило б пошкодження даних для глобальних застосунків замість гучної, перехоплюваної помилки.

Сильна відповідь включає

  • наївний datetime взагалі не має прикріпленого часового поясу
  • обізнаний datetime несе tzinfo, що прив'язує до зміщення UTC
  • == і != між наївним і обізнаним просто дають нерівність; порядок і віднімання кидають TypeError замість вгадування

Практичний приклад

`datetime.now()` повертає наївний datetime у локальному часі сервера, тоді як `datetime.now(timezone.utc)` повертає обізнаний; віднімання одного від іншого викликає `TypeError` замість тонко неправильної тривалості.

#datetime#gotchas

Джерела

The Python Standard LibraryPython Software Foundation · Authoritative behavior for built-in types and standard library modules
Web, automation and QA-relevant PythonJuniorVery commonPractical

Чому більшість розробників Python обирають сторонню бібліотеку `requests` замість `urllib` зі стандартної бібліотеки?

Відповідь

`urllib.request` — частина стандартної бібліотеки й працює без встановлення чогось, але його API багатослівний і низькорівневий: побудова запиту, обробка перенаправлень, декодування JSON і керування куками кожне вимагає кількох явних кроків. `requests` обгортає ту саму базову можливість у набагато вищий, зручніший API — `requests.get(url).json()` — що за замовчуванням розумно обробляє перенаправлення, кодування й куки. Один верхньорівневий виклик на кшталт `requests.get(url)` все ще відкриває власне короткочасне з'єднання; саме `requests.Session()` тримає пул з'єднань живим і повторно використовує TCP/TLS-з'єднання та спільну конфігурацію на кшталт заголовків і куків для кількох викликів до того самого хоста. У будь-якому разі `requests` став де-факто стандартом для HTTP-викликів у Python, попри те, що не входить до стандартної бібліотеки.

Сильна відповідь включає

  • urllib — стандартна бібліотека, але багатослівна й низькорівнева
  • requests обгортає ту саму можливість у значно зручніший API
  • окремий виклик requests.get() відкриває власне з'єднання; лише Session() повторно використовує з'єднання й конфігурацію

Практичні приклади

Повторно використовуваний HTTP-клієнт через Session
import requests

with requests.Session() as session:
    session.headers.update({"Accept": "application/json"})
    response = session.get(
        "https://api.example.test/users/42",
        timeout=5,
    )
    response.raise_for_status()
    user = response.json()
    print(user["id"])

Session повторно використовує connection pool і спільні headers/cookies між викликами. timeout та raise_for_status() роблять помилки явними замість нескінченного очікування чи тихого прийняття error response.

Очікуваний результат: Для успішної JSON-відповіді на кшталт {'id': 42} друкує 42; network/status помилки викликають exceptions.

#requests#http

Джерела

The Python Standard LibraryPython Software Foundation · Authoritative behavior for built-in types and standard library modulesRequests: HTTP for HumansPython Software Foundation (Requests project) · Third-party HTTP client API, including Session-based connection pooling and configuration reuse
Web, automation and QA-relevant PythonMiddleCommonTheory

Чим Flask і FastAPI відрізняються за основним дизайном, окрім того, що обидва описують як «легкі веб-фреймворки Python»?

Відповідь

Flask — синхронний WSGI-фреймворк, побудований навколо мінімалізму й явної маршрутизації; валідація запитів, серіалізація й документація опційні через розширення, а не вбудовані в ядро. FastAPI побудований на ASGI для нативної підтримки `async`/`await` і безпосередньо інтегрує моделі Pydantic у визначення маршрутів, тож анотації типів обробника запиту одночасно визначають валідацію вхідних даних, автоматичні відповіді про помилки для некоректних запитів і інтерактивну документацію OpenAPI, без окремого кроку визначення схеми.

Сильна відповідь включає

  • Flask синхронний WSGI, мінімальне ядро, валідація опційна через розширення
  • FastAPI на основі ASGI з нативною підтримкою async/await
  • анотації типів FastAPI одночасно валідують через Pydantic й генерують OpenAPI-документацію

Практичний приклад

`@app.post("/users")\nasync def create_user(user: UserIn) -> UserOut:` у FastAPI автоматично валідує вхідний JSON проти `UserIn`, тоді як еквівалентний маршрут Flask потребує явного виклику бібліотеки валідації всередині тіла функції.

#web-frameworks#flask#fastapi

Джерела

Flask documentationPallets Projects · Minimal WSGI web framework request/response and routing modelFastAPI documentationFastAPI · Async web framework request validation and dependency-injection model
Web, automation and QA-relevant PythonSeniorOccasionalTheory

Яка різниця між WSGI і ASGI, і чому екосистема веброзробки Python потребувала нового стандарту?

Відповідь

WSGI (Web Server Gateway Interface) — старіший стандартний інтерфейс між синхронним вебзастосунком Python і вебсервером: він обробляє рівно один запит за раз на потік/процес-воркер і не має нативного поняття асинхронного коду, WebSocket чи довготривалих з'єднань. ASGI (Asynchronous Server Gateway Interface) розширює ту саму ідею для підтримки застосунків на основі `async`/`await`, дозволяючи одному воркеру кооперативно обробляти багато паралельних з'єднань, включно з WebSocket і подіями, надісланими сервером, чого синхронна модель WSGI «один запит за раз» взагалі не може виразити.

Сильна відповідь включає

  • WSGI синхронний, один запит на воркер за раз, без нативного async чи WebSocket
  • ASGI розширює інтерфейс для підтримки async/await і довготривалих з'єднань
  • ASGI уможливлює WebSocket і паралельні з'єднання на воркер, чого WSGI не може

Практичний приклад

Застосунок Flask на основі WSGI потребує окремої бібліотеки й архітектури для підтримки WebSocket-чату, тоді як застосунок FastAPI на основі ASGI може визначити маршрут WebSocket прямо поряд зі звичайними HTTP-маршрутами.

#wsgi#asgi#web-frameworks

Джерела

FastAPI documentationFastAPI · Async web framework request validation and dependency-injection modelFlask documentationPallets Projects · Minimal WSGI web framework request/response and routing model
Python browser automationMiddleVery commonPractical

У прив'язках Selenium для Python яка різниця між неявним (implicit) і явним (explicit) очікуванням, і чому не рекомендують їх змішувати?

Відповідь

Неявне очікування (`driver.implicitly_wait(10)`) застосовує єдиний глобальний тайм-аут до кожного виклику `find_element` на решту життя драйвера, тихо опитуючи до цього часу, коли елемент не з'являється одразу. Явне очікування (`WebDriverWait(driver, 10).until(condition)`) чекає конкретної, названої умови — елемент видимий, клікабельний, текст присутній — в одній конкретній точці тесту, даючи набагато точніший і читабельніший намір, ніж загальний тайм-аут. Власна документація Selenium явно застерігає від змішування неявних і явних очікувань в одному тесті, бо їхні тайм-аути можуть непередбачувано взаємодіяти й давати нестабільні, важкі для налагодження часи очікування.

Сильна відповідь включає

  • неявне очікування встановлює один глобальний тайм-аут для кожного find_element
  • явне очікування націлене на конкретну названу умову в одній точці тесту
  • змішування обох не рекомендується через непередбачувану взаємодію тайм-аутів

Практичні приклади

Очікування конкретної UI-умови
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait

login = WebDriverWait(driver, 10).until(
    EC.element_to_be_clickable((By.CSS_SELECTOR, "[data-testid='login']"))
)
login.click()

Explicit wait прямо описує, що саме має стати істинним у цій точці тесту. Це локальніше й легше діагностується, ніж один implicit timeout для кожного пошуку елемента.

Очікуваний результат: Клік відбувається лише після появи й клікабельності login-елемента, або через 10 секунд виникає TimeoutException.

#selenium#test-automation

Джерела

Selenium with PythonSelenium · WebDriver automation patterns used from Python test code
Python browser automationMiddleVery commonDesign

Що таке патерн Page Object Model в автоматизації тестування, і яку проблему підтримки він вирішує?

Відповідь

Page Object Model обгортає кожну сторінку чи значущий компонент застосунку у власний клас Python, що надає методи для дій, потрібних тесту (`login_page.enter_username(...)`, `login_page.submit()`), тримаючи сирі локатори (`By.ID`, `By.CSS_SELECTOR`, ...) інкапсульованими всередині об'єкта, що володіє цією частиною UI, а не розкиданими по десятках тестових файлів. Основна мета — централізувати й інкапсулювати це знання про локатори, а не сувора вимога, щоб кожен локатор був приватним атрибутом чи щоб оновлення потрібне було рівно одному класу — великі набори часто розбивають сторінку на кілька об'єктів-компонентів чи ділять локатори між тісно пов'язаними сторінками, зберігаючи при цьому базовий принцип. Коли розмітка застосунку змінюється, виправлення живе в об'єкті(ах), що володіють цією частиною UI, замість пошуку й виправлення кожного тестового файлу, що випадково посилався на той локатор напряму, а це саме той тягар підтримки, через який набори UI-тестів дорого підтримувати актуальними в міру еволюції застосунку.

Сильна відповідь включає

  • один клас на сторінку/компонент надає методи дій, інкапсулює локатори
  • мета — централізувати знання про локатори, не сувора вимога одного класу
  • великі набори часто розбиваються на кілька об'єктів-компонентів зі спільними чи вузькими локаторами

Практичний приклад

Якщо CSS-селектор кнопки входу змінюється з `#submit` на `#login-btn`, набір з Page Object Model виправляє це в одному класі `LoginPage`, тоді як набір без цього патерну міг би потребувати того самого виправлення в п'ятдесяти окремих тестових функціях.

#test-automation#design-patterns

Джерела

Selenium with PythonSelenium · WebDriver automation patterns used from Python test code
Pytest & fixturesMiddleCommonPractical

Яка мета файлу `conftest.py` у проєкті автоматизації тестування на основі pytest?

Відповідь

`conftest.py` — спеціальний файл, який pytest автоматично виявляє й завантажує без явного імпорту, використовується для визначення фікстур, хуків і плагінів, які мають бути спільними для кількох тестових файлів у тому самому каталозі (і його підкаталогах), без потреби кожному тестовому файлу індивідуально їх імпортувати. Розміщення спільних фікстур на кшталт налаштованого екземпляра WebDriver, автентифікованого API-клієнта чи фабрик тестових даних у `conftest.py` уникає дублювання цієї логіки налаштування в десятках тестових модулів, а кілька файлів `conftest.py` на різних рівнях каталогів дозволяють ширшим, дорогим фікстурам жити біля кореня проєкту, тоді як вужчі, специфічні для функціональності — ближче до тестів, що їх використовують.

Сильна відповідь включає

  • автоматично виявляється pytest, без явного імпорту
  • поширює фікстури/хуки/плагіни на всі тестові файли в дереві каталогу
  • кілька conftest.py на різних рівнях відповідно масштабують спільне налаштування

Практичний приклад

Кореневий `conftest.py` може надавати фікстуру `browser`, спільну для кожного UI-тесту, тоді як `tests/checkout/conftest.py` додає специфічну для оформлення замовлення фікстуру `cart_with_items`, що використовується лише тестами в цьому підкаталозі.

#pytest#test-automation

Джерела

pytest documentationpytest · Fixture scope, parametrization, markers and plugin architecture
Python API & service automationMiddleCommonPerformance

Чому повторне використання об'єкта `requests.Session()` для кількох викликів до того самого хоста покращує продуктивність API-тестів?

Відповідь

`requests.Session()` тримає базовий пул з'єднань живим між викликами, тож повторні запити до того самого хоста повторно використовують уже встановлене TCP-з'єднання (і TLS-рукостискання для HTTPS) замість повної оплати вартості встановлення з'єднання на кожен окремий виклик. Сесія також дозволяє встановити спільну конфігурацію один раз — заголовки, автентифікацію, куки — замість повторення її на кожному окремому виклику `requests.get(...)`, що одночасно пришвидшує великий набір API-тестів і знижує ризик випадкового надсилання неузгоджених заголовків між різними запитами.

Сильна відповідь включає

  • Session тримає живим пул з'єднань, повторно використовуючи TCP/TLS між запитами
  • уникає повної вартості встановлення з'єднання на кожен окремий виклик
  • також централізує спільну конфігурацію на кшталт заголовків і автентифікації

Практичний приклад

Виконання 500 перевірок API-тестів проти того самого хоста з повторно використаною `session = requests.Session()` вимірювано швидше за 500 незалежних викликів `requests.get(...)`, кожен з яких відкриває нове з'єднання.

#requests#performance#test-automation

Джерела

The Python Standard LibraryPython Software Foundation · Authoritative behavior for built-in types and standard library modules
Mocking & test isolationMiddleCommonPractical

Чому мокування HTTP-викликів важливе в unit-тестах, і яку проблему воно вирішує порівняно з викликом реального API?

Відповідь

Виклик реального зовнішнього API з unit-тесту робить тест повільним, нестабільним (може провалюватися через мережеві проблеми чи простій третьої сторони, а не реальний баг) і залежним від зовнішнього стану, який автор тесту не контролює, як-от обмеження частоти запитів чи дані, що змінюються з часом. Мокування шару HTTP — використання `unittest.mock.patch` на коді, що викликає, чи бібліотеки на кшталт `responses`/`requests-mock`, що перехоплює виклики `requests` на рівні транспорту — дозволяє тесту точно перевірити, як код, що тестується, поводиться для заданої, контрольованої відповіді, включно з граничними випадками на кшталт помилки 500 чи тайм-ауту, які було б важко чи небезпечно викликати проти реального сервісу за запитом.

Сильна відповідь включає

  • реальні API-виклики роблять тести повільними, нестабільними, залежними від неконтрольованого стану
  • мокування перехоплює шар HTTP, повертаючи контрольовану, наперед задану відповідь
  • дозволяє тестам надійно перевіряти граничні випадки на кшталт помилок чи тайм-аутів

Практичний приклад

`responses.add(responses.GET, "https://api.example.com/user", json={"id": 1}, status=200)` дозволяє тесту перевірити поведінку коду для успішної відповіді, а в іншому тесті — для симульованого `status=503`, ніколи не звертаючись до реального API.

#testing#mocking#http

Джерела

unittest — Unit testing frameworkPython Software Foundation · Baseline test-case, mock and assertion semantics shared across Python test toolingpytest documentationpytest · Fixture scope, parametrization, markers and plugin architecture
Python AQA framework & CIJuniorCommonPractical

Чому проєкт автоматизації тестування має читати конфігурацію на кшталт базових URL і облікових даних зі змінних середовища, а не жорстко прописувати їх у тестових файлах?

Відповідь

Жорстко прописані URL і облікові дані прив'язують кожен прогін тестів до одного конкретного середовища й змушують редагувати вихідний код тестів лише щоб спрямувати набір на staging проти production проти локального інстансу, а жорстко прописані секрети ризикують потрапити в систему контролю версій. Читання конфігурації через `os.environ` (часто завантажене з локального файлу `.env` бібліотекою на кшталт `python-dotenv` для зручності розробника) дозволяє тому самому коду тестів запускатися незмінним проти будь-якого цільового середовища, з секретами, наданими сховищем секретів системи CI, а не тими, що коли-небудь з'являються в репозиторії.

Сильна відповідь включає

  • жорстке прописування прив'язує прогони до одного середовища й ризикує секретами в репозиторії
  • os.environ дозволяє тому самому коду тестів працювати з будь-яким середовищем незмінним
  • секрети надходять зі сховища секретів CI, ніколи з файлів під контролем версій

Практичний приклад

`BASE_URL = os.environ["BASE_URL"]`, прочитаний один раз на початку модуля конфігурації тестів, дозволяє тому самому набору тестів працювати проти `https://staging.example.com` чи `https://example.com` лише зміною змінної середовища в CI-задачі, ніколи не коду тестів.

#test-automation#configuration

Джерела

The Python Standard LibraryPython Software Foundation · Authoritative behavior for built-in types and standard library modules
Python browser automationMiddleCommonPractical

Що означає запуск браузера в «headless»-режимі для автоматизації тестування Selenium чи Playwright, і чому це важливо для CI?

Відповідь

Headless-браузер запускає рушій рендерингу й JavaScript без відкриття видимого вікна чи поверхні відображення з апаратним прискоренням, що дозволяє йому працювати на CI-сервері чи в контейнері взагалі без підключеного дисплея, і він зазвичай швидший і легший за пам'яттю, ніж повна графічна сесія браузера, бо не потрібно малювати пікселі на екрані. Проте не гарантовано, що він поводитиметься ідентично сесії з інтерфейсом у кожному аспекті: сучасний Playwright, наприклад, за замовчуванням у деяких конфігураціях використовує окрему збірку Chromium «headless shell», і власна документація зазначає, що headless-режим, режим з інтерфейсом і різні канали браузера можуть рендерити чи поводитися дещо по-різному для окремих функцій. Для більшості функціональної автоматизації тестування різниця непомітна, але команди, що працюють з візуально чутливими чи чутливими до функцій браузера потоками, часто тримають у CI також лінію з режимом з інтерфейсом чи стабільним каналом, а не припускають повну гарантовану паритетність.

Сильна відповідь включає

  • запускається без видимого вікна чи дисплея, дозволяючи CI без підключеного дисплея
  • не гарантовано ідентичну поведінку режиму з інтерфейсом у кожному аспекті — про це каже документація сучасних інструментів
  • чутливі до візуалу чи функцій набори часто тримають лінію з інтерфейсом/стабільним каналом, не покладаючись на повну паритетність

Практичний приклад

Раннер GitHub Actions не має сервера дисплея, тож `webdriver.Chrome(options=headless_options)` обов'язковий, щоб набір узагалі запустився в цьому конвеєрі, тоді як розробник, що налагоджує проблемний тест локально, часто тимчасово перемикається назад у режим з інтерфейсом, щоб спостерігати за діями браузера.

#selenium#ci-cd#test-automation

Джерела

Selenium with PythonSelenium · WebDriver automation patterns used from Python test code
Pytest & fixturesMiddleCommonPractical

Чим у pytest відрізняються markers, вибір через `-m`, `skip` і `xfail`, і як їх використовувати в реальному automation suite?

Відповідь

Custom markers додають тестам metadata на кшталт `smoke`, `api` або `slow` і дозволяють CI запускати потрібні subsets через `pytest -m`; markers варто реєструвати, щоб typo не створював новий mark непомітно. `skip` означає, що тест за поточної умови не слід виконувати, а `xfail` документує відомий expected failure і може бути strict, щоб unexpected pass був видимим. Markers мають визначати execution slices, а не безстроково приховувати нестабільні тести; причини skip/xfail повинні бути явними й переглядатися.

Сильна відповідь включає

  • відрізняє metadata та selection від результатів виконання
  • пояснює skip проти expected failure і користь strict xfail
  • реєструє custom markers і не використовує їх для постійного приховування flakiness

Практичний приклад

PR job запускає `pytest -m "smoke and not external"`, а nightly job виконує повний regression set. Тест, заблокований недоступним third-party sandbox, використовує `skipif` з явною умовою; тест на підтверджений product defect має прив'язаний до ticket strict `xfail`, щоб команда помітила виправлення дефекту.

#pytest#markers#xfail#test-selection

Джерела

pytest documentationpytest · Fixture scope, parametrization, markers and plugin architecturepytest Interview QuestionsAssertHired · 2026 Python QA/SDET recurrence signal for pytest fixtures, markers, monkeypatch, xdist, reporting and framework structure
Mocking & test isolationMiddleCommonPractical

Коли варто використовувати pytest `monkeypatch` замість `unittest.mock.patch`, і що саме потрібно patch-ити?

Відповідь

`monkeypatch` зручний, коли pytest-тесту треба тимчасово змінити attributes, dictionaries, environment variables, paths або working directory; pytest автоматично відновить ці зміни після завершення відповідного scope. `unittest.mock.patch` сильніший, коли потрібні call assertions, autospec або детальний контроль поведінки Mock. В обох випадках patch-ити треба ім'я, яке код під тестом реально lookup-ить, а не механічно об'єкт у місці його початкового визначення; якщо patching стає основою дизайну, краще розглянути явну dependency injection.

Сильна відповідь включає

  • використовує monkeypatch для тимчасового pytest-managed state на кшталт env vars та attributes
  • обирає mock.patch, коли потрібні call verification або багатша Mock-поведінка
  • patch-ить у місці lookup залежності й розуміє надмірний patching як design smell

Практичний приклад

Configuration test використовує `monkeypatch.setenv("API_URL", "https://stub")`, бо потрібно лише тимчасово змінити process state. Service test використовує `patch("orders.client.send", autospec=True)`, бо треба перевірити, що `send()` викликано один раз з точним payload.

#pytest#monkeypatch#mocking#unittest-mock

Джерела

pytest documentationpytest · Fixture scope, parametrization, markers and plugin architecturepytest Interview QuestionsAssertHired · 2026 Python QA/SDET recurrence signal for pytest fixtures, markers, monkeypatch, xdist, reporting and framework structureunittest — Unit testing frameworkPython Software Foundation · Baseline test-case, mock and assertion semantics shared across Python test tooling
Python AQA reliabilitySeniorCommonDesign

Що зазвичай ламається після паралелізації pytest suite через `pytest-xdist`, і як спроєктувати suite так, щоб він залишався надійним?

Відповідь

`pytest-xdist` виконує тести в окремих worker processes, тому прихований shared state швидко стає проблемою: повторне використання accounts, фіксовані filenames або ports, спільні database rows, order dependencies і session fixtures, які розраховували на один process. Тести мають незалежно створювати й очищати data, за потреби отримувати worker-specific resources, не залежати від порядку виконання та використовувати distribution mode, що враховує дорогі або shared fixtures. Parallelism має виявляти coupling, а не маскуватися sleeps чи широкими retries.

Сильна відповідь включає

  • розуміє, що xdist workers є окремими processes з незалежним pytest execution
  • називає shared data, fixed resources та order dependence типовими причинами parallel failures
  • використовує isolated test data або worker-specific resources і розуміє distribution modes

Практичний приклад

Після переходу на `pytest -n 8` account tests починають випадково падати, бо всі workers змінюють одного користувача. Правильне рішення — створювати unique user на worker або test і детерміновано його очищати; перехід на `-n 2` лише робить race рідшим, але не усуває причину.

#pytest#pytest-xdist#parallel-testing#test-isolation

Джерела

pytest-xdist documentationpytest-dev · Authoritative behavior for distributed pytest execution, workers, scheduling and parallel-test isolation constraintspytest Interview QuestionsAssertHired · 2026 Python QA/SDET recurrence signal for pytest fixtures, markers, monkeypatch, xdist, reporting and framework structure50 SDET Interview Questions (With Sample Answers) 2026sdet.qa · 2026 recurrence signal for framework design, test-data isolation, flaky-test handling, API testing and practical coding
Python AQA reliabilityMiddleVery commonTroubleshooting

Python automation test flaky лише в CI або під parallel execution. Як його досліджувати замість простого додавання retries?

Відповідь

Спочатку зробіть failure спостережуваним: збережіть traceback, logs, request/response evidence, browser trace або screenshot, test seed і worker identity. Відтворіть ті самі environment та execution mode, а потім системно перевірте типові причини: fixed sleeps, stale або brittle locators, shared state, test-order dependence, data collisions, asynchronous completion та нестабільність environment. Відомий flaky test за потреби можна тимчасово quarantine з blocking gate, але з owner і строком виправлення; retries допустимі лише як видима й вимірювана mitigation, а не як root-cause strategy.

Сильна відповідь включає

  • збирає достатньо CI evidence для відтворення точних умов failure
  • перевіряє timing, shared state, ordering, selectors та environment замість припущення про product bug
  • трактує quarantine або retries як контрольовану mitigation з ownership, а не як fix

Практичний приклад

Checkout test стабільний локально, але падає у 3% запусків на чотирьох CI workers. Trace показує, що два workers використовують один cart account і перезаписують state один одного. Ізольовані users усувають flake; `reruns=3` лише приховав би race і створив хибне враження стабільного pipeline.

#pytest#flaky-tests#ci#troubleshooting

Джерела

pytest documentationpytest · Fixture scope, parametrization, markers and plugin architecture50 SDET Interview Questions (With Sample Answers) 2026sdet.qa · 2026 recurrence signal for framework design, test-data isolation, flaky-test handling, API testing and practical codingThe Complete 2026 SDET Interview Preparation GuideScrollTest · 2026 interview signal for Selenium dynamic elements, Playwright, framework design, test data and CI-oriented automationStrong Middle / Senior Automation Test Engineer (Python)ELEKS via DOU Jobs · 2026 Ukrainian-market evidence for Python framework design, Selenium, Playwright, REST, CI/CD, flake-rate ownership and test-layer architecture
Pytest & fixturesSeniorVery commonDesign

Як спроєктувати maintainable Python test automation framework з нуля?

Відповідь

Почніть з product risks і execution needs, а потім розділіть responsibilities: pytest test layer, reusable domain/page або API-client layer, fixtures і lifecycle, configuration, test-data factories, assertions, diagnostics/reporting та CI integration. Тести мають бути тонкими й business-readable; не створюйте один global utility module або inheritance tree, який відповідає за все, робіть dependencies явними й одразу проєктуйте independent local та parallel execution. Сильний framework — це не folder template, а boundaries, що роблять зміни, debugging і ownership передбачуваними.

Сильна відповідь включає

  • визначає чіткі layers для tests, interactions або clients, fixtures, data, configuration, diagnostics та CI
  • тримає tests тонкими й уникає god utilities та deep inheritance
  • проєктує isolation, parallel execution, maintainability та observable failures

Практичний приклад

Для продукту з REST APIs і browser UI pytest керує execution; typed API clients та page/component objects надають business actions; fixtures створюють users і очищають state; data factories генерують unique records; config залежить від environment; failure hooks додають logs/traces; CI вибирає smoke або regression і безпечно запускає subsets паралельно.

#pytest#test-automation#framework-design#architecture

Джерела

50 SDET Interview Questions (With Sample Answers) 2026sdet.qa · 2026 recurrence signal for framework design, test-data isolation, flaky-test handling, API testing and practical codingThe Complete 2026 SDET Interview Preparation GuideScrollTest · 2026 interview signal for Selenium dynamic elements, Playwright, framework design, test data and CI-oriented automationStrong Middle / Senior Automation Test Engineer (Python)ELEKS via DOU Jobs · 2026 Ukrainian-market evidence for Python framework design, Selenium, Playwright, REST, CI/CD, flake-rate ownership and test-layer architectureКомпоненти фреймворку автоматизації тестування за допомогою Selenium та PythonDOU · Practitioner evidence for Python/Selenium framework architecture, reusable components and automation design choices
Python AQA reliabilityMiddleCommonDesign

Як керувати test data у Python automation framework, щоб тести залишалися незалежними та parallel-safe?

Відповідь

Використовуйте factories або builder helpers, які створюють мінімально валідний object і дозволяють кожному test перевизначити лише важливі поля. Provision data робіть через найшвидший надійний layer — часто API або test-support interfaces замість UI setup — давайте кожному тесту unique identifiers і забезпечуйте deterministic cleanup або disposable namespaces. Fixtures мають керувати lifecycle, а factories — описувати data; shared mutable 'test user' чи один великий static JSON роблять suite order-dependent і небезпечним для parallel execution.

Сильна відповідь включає

  • відокремлює construction data через factories від lifecycle management через fixtures
  • створює unique independent state і віддає перевагу швидким setup paths замість UI setup
  • планує cleanup або disposable data, щоб parallel tests не конфліктували

Практичний приклад

Замість десяти tests зі спільним `qa_user@example.com`, `user_factory()` створює unique account через API, а fixture реєструє cleanup. Тепер ті самі tests можна запускати в будь-якому порядку й на восьми workers без конфліктів через balances, permissions або passwords.

#pytest#test-data#factories#test-isolation

Джерела

50 SDET Interview Questions (With Sample Answers) 2026sdet.qa · 2026 recurrence signal for framework design, test-data isolation, flaky-test handling, API testing and practical codingThe Complete 2026 SDET Interview Preparation GuideScrollTest · 2026 interview signal for Selenium dynamic elements, Playwright, framework design, test data and CI-oriented automationКомпоненти фреймворку автоматизації тестування за допомогою Selenium та PythonDOU · Practitioner evidence for Python/Selenium framework architecture, reusable components and automation design choices
Python browser automationMiddleCommonTroubleshooting

Що спричиняє Selenium `StaleElementReferenceException` у dynamic applications і як це правильно виправляти?

Відповідь

Selenium WebElement посилається на конкретний DOM node; reference стає stale, коли navigation, re-rendering або JavaScript замінює чи detach-ить цей node. Не треба обгортати весь test у catch або додавати довільний sleep. Повторно знайдіть element після state change, дочекайтеся реальної condition, за якої новий element готовий до дії, і покращте page/component abstraction так, щоб вона зберігала locator intent, а не довгоживучі dynamic WebElement instances там, де це доречно.

Сильна відповідь включає

  • пояснює staleness як збережений reference на DOM node, який було замінено або detached
  • повторно знаходить element після відповідного state transition і чекає meaningful condition
  • уникає blanket retries, sleeps і довгоживучих dynamic WebElement references

Практичний приклад

React table re-render-иться після filtering. Test зберіг row WebElement до натискання Filter і потім використав його повторно, отримавши stale reference. Виправлення — дочекатися завершення filtering і знову знайти row за stable business attribute, а не retry-ити сам stale object.

#selenium#stale-element#dynamic-ui#waits

Джерела

Selenium with PythonSelenium · WebDriver automation patterns used from Python test codeThe Complete 2026 SDET Interview Preparation GuideScrollTest · 2026 interview signal for Selenium dynamic elements, Playwright, framework design, test data and CI-oriented automationStrong Middle / Senior Automation Test Engineer (Python)ELEKS via DOU Jobs · 2026 Ukrainian-market evidence for Python framework design, Selenium, Playwright, REST, CI/CD, flake-rate ownership and test-layer architecture
Python browser automationMiddleVery commonPractical

Як Playwright locators та auto-waiting змінюють написання Python UI tests порівняно з Selenium-style explicit waits?

Відповідь

Playwright locator повторно resolve-иться під час використання і бере участь у retryable actions та assertions. Перед action на кшталт click Playwright чекає релевантних actionability conditions — visibility, stability, event reception та enabled state; web-first assertions retry-яться до виконання condition. Віддавайте перевагу user-facing locators на кшталт role, label або test id замість brittle DOM chains і не замінюйте waiting model Playwright на `time.sleep()` чи довільні timeouts, якщо це не діагностика конкретної проблеми.

Сильна відповідь включає

  • пояснює locator re-resolution та actionability, а не лише фразу що Playwright чекає автоматично
  • обирає semantic user-facing locators замість brittle CSS або XPath structure
  • використовує retrying assertions і не покладається на fixed sleeps для synchronization

Практичний приклад

Для Save button `page.get_by_role("button", name="Save").click()` дозволяє Playwright дочекатися, поки саме ця кнопка стане actionable. Далі `expect(page.get_by_text("Saved")).to_be_visible()` retry-ить assertion; `time.sleep(2)` між ними зазвичай лише робить test повільнішим і менш deterministic.

#playwright#locators#auto-waiting#ui-automation

Джерела

Playwright for Python documentationMicrosoft Playwright · Authoritative Python behavior for locators, auto-waiting, browser contexts, network controls and test isolationPlaywright Interview Questions for SDET 2026AI Test Automation Hub · 2026 interview signal for Playwright auto-waiting, semantic locators, browser contexts, network interception and debuggingPlaywright Interview Questions and Answers (2026 Guide)The Testing Academy · 2026 recurrence signal for Playwright locators, auto-waiting, fixtures, network interception and hands-on debuggingThe Complete 2026 SDET Interview Preparation GuideScrollTest · 2026 interview signal for Selenium dynamic elements, Playwright, framework design, test data and CI-oriented automation
Python AQA reliabilityMiddleCommonDesign

Що таке Playwright `BrowserContext` і чому він є центральним для test isolation та parallel execution?

Відповідь

BrowserContext — це ізольована browser session всередині одного browser process, подібна до нового incognito profile: cookies, local storage, permissions та інший session state відокремлені від інших contexts. Fresh context на кожен test дає сильну isolation без запуску повного browser щоразу, тоді як кілька pages всередині одного context навмисно ділять session state. Для незалежних users або tests використовуйте окремі contexts, а authentication reuse робіть через явний storage-state setup, а не через витік state між тестами.

Сильна відповідь включає

  • описує BrowserContext як ізольований browser-session state, а не просто ще одну page
  • пов'язує fresh contexts з дешевою test isolation та safe parallelism
  • використовує окремі contexts для independent users і explicit storage state для контрольованого auth reuse

Практичний приклад

Messaging test потребує одночасно Alice і Bob. Він створює два contexts, окремо логінить кожного user і відкриває по одній page у кожному context. Їх cookies та local storage не перезаписують один одного, хоча обидві sessions ефективно працюють в одному browser process.

#playwright#browser-context#test-isolation#parallel-testing

Джерела

Playwright for Python documentationMicrosoft Playwright · Authoritative Python behavior for locators, auto-waiting, browser contexts, network controls and test isolationPlaywright Interview Questions for SDET 2026AI Test Automation Hub · 2026 interview signal for Playwright auto-waiting, semantic locators, browser contexts, network interception and debuggingPlaywright Interview Questions and Answers (2026 Guide)The Testing Academy · 2026 recurrence signal for Playwright locators, auto-waiting, fixtures, network interception and hands-on debugging
Data types and structuresJuniorVery commonPractical

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

Відповідь

Використайте set для запам'ятовування вже побачених значень і list для впорядкованого результату. Для кожного елемента додавайте його до результату лише якщо його ще немає в set, а потім додавайте до set. Це зберігає порядок першої появи й дає в середньому O(1) для перевірки належності, тому весь прохід має середню складність O(n).

Сильна відповідь включає

  • використовує set для середнього O(1) пошуку
  • зберігає порядок першої появи
  • один прохід із середньою складністю O(n)

Практичні приклади

Дедуплікація зі збереженням порядку за один прохід
def unique_in_order(items):
    seen = set()
    result = []

    for item in items:
        if item not in seen:
            seen.add(item)
            result.append(item)

    return result

print(unique_in_order([3, 1, 3, 2, 1, 4]))

seen забезпечує швидку перевірку належності, а result зберігає порядок вставки. Версія припускає, що вхідні значення є hashable.

Очікуваний результат: [3, 1, 2, 4]

#lists#sets#algorithms

Джерела

The Python Standard LibraryPython Software Foundation · Authoritative behavior for built-in types and standard library modulesThe Python TutorialPython Software Foundation · Baseline syntax, control flow, functions and module basics
Data types and structuresJuniorCommonPractical

Маючи список словників, згрупуйте записи за одним полем без повторної перевірки, чи вже існує кожна група.

Відповідь

`defaultdict(list)` добре підходить, бо відсутній ключ автоматично створює порожній список перед першим append. Один раз пройдіться по записах і додавайте кожен запис до списку за значенням вибраного поля. Рішення коротке, не потребує явних перевірок існування ключа й має середню складність O(n) для n записів.

Сильна відповідь включає

  • правильно використовує defaultdict(list) або setdefault
  • один прохід по записах
  • пояснює відсутність потреби в явній перевірці ключа

Практичні приклади

Групування результатів тестів за suite
from collections import defaultdict

results = [
    {"suite": "api", "name": "create user"},
    {"suite": "ui", "name": "login"},
    {"suite": "api", "name": "delete user"},
]

grouped = defaultdict(list)
for result in results:
    grouped[result["suite"]].append(result)

print([item["name"] for item in grouped["api"]])

defaultdict(list) створює список групи при першому зверненні, тому циклу потрібен лише один append.

Очікуваний результат: ['create user', 'delete user']

#dict#collections#grouping

Джерела

collections — Container datatypesPython Software Foundation · deque, defaultdict, Counter, namedtuple and ChainMap semantics
Data types and structuresJuniorVery commonPractical

Розгорніть список списків на один рівень ідіоматичним способом Python.

Відповідь

Вкладений list comprehension є коротким рішенням, коли вхідні дані мають рівно один рівень вкладеності: спочатку ітеруйте внутрішні колекції, а потім кожен елемент усередині них. Comprehension зберігає порядок зліва направо й створює один новий список. Це не слід подавати як рекурсивне рішення для довільної глибини вкладеності.

Сильна відповідь включає

  • правильно використовує вкладений comprehension
  • зберігає порядок
  • відрізняє один рівень від рекурсивного flatten

Практичні приклади

Вкладений comprehension
nested = [[1, 2], [], [3, 4]]
flat = [item for group in nested for item in group]
print(flat)

Перший `for` обирає кожен внутрішній список; другий додає кожен його елемент у результат.

Очікуваний результат: [1, 2, 3, 4]

#lists#comprehensions#algorithms

Джерела

The Python TutorialPython Software Foundation · Baseline syntax, control flow, functions and module basics
Standard library and toolingJuniorVery commonPractical

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

Відповідь

`collections.Counter` спеціально призначений для таблиць частот. Створіть його безпосередньо з iterable і використайте `most_common(2)` замість ручного підтримання словника та окремого сортування. Counter залишається підкласом dict, тому звичайний доступ за ключем працює, а спеціалізований API чітко передає намір.

Сильна відповідь включає

  • використовує collections.Counter
  • використовує most_common для ранжування
  • розуміє Counter як спеціалізований frequency mapping

Практичні приклади

Таблиця частот через Counter
from collections import Counter

statuses = ["passed", "failed", "passed", "skipped", "failed", "passed"]
counts = Counter(statuses)

print(counts)
print(counts.most_common(2))

Counter рахує за один прохід, а `most_common` повертає пари `(value, count)` у порядку спадання частоти.

Очікуваний результат: Counter({'passed': 3, 'failed': 2, 'skipped': 1}) і [('passed', 3), ('failed', 2)].

#counter#collections#counting

Джерела

collections — Container datatypesPython Software Foundation · deque, defaultdict, Counter, namedtuple and ChainMap semantics
Strings and text processingMiddleCommonPractical

Поверніть перший символ рядка, що не повторюється, або `None`, якщо всі символи повторюються.

Відповідь

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

Сильна відповідь включає

  • спочатку рахує символи
  • робить другий впорядкований прохід
  • повертає None, якщо унікального символу немає

Практичні приклади

Спочатку підрахунок, потім збереження початкового порядку
from collections import Counter

def first_unique(text):
    counts = Counter(text)
    return next((char for char in text if counts[char] == 1), None)

print(first_unique("swiss"))
print(first_unique("aabb"))

Generator expression ліниво переглядає символи в початковому порядку, а `next(..., None)` задає результат, коли збігу немає.

Очікуваний результат: w, потім None.

#strings#counter#algorithms

Джерела

collections — Container datatypesPython Software Foundation · deque, defaultdict, Counter, namedtuple and ChainMap semanticsThe Python Standard LibraryPython Software Foundation · Authoritative behavior for built-in types and standard library modules
Strings and text processingMiddleCommonPractical

Розберіть log-рядки на кшталт `2026-08-22 ERROR timeout` на timestamp, level і message, відхиляючи некоректні рядки.

Відповідь

Скомпілюйте один прив'язаний regular expression із named capture groups і використайте `fullmatch`, щоб часткове сміття не приймалось тихо. Для валідних рядків поверніть named groups, а для некоректних — явний sentinel, наприклад `None`. Одноразова компіляція особливо корисна, коли тим самим pattern обробляється багато рядків.

Сильна відповідь включає

  • використовує anchored/full-match regex
  • використовує named capture groups
  • явно обробляє некоректний input

Практичні приклади

Строгий log parser із named groups
import re

LOG_LINE = re.compile(
    r"(?P<date>\d{4}-\d{2}-\d{2}) (?P<level>INFO|WARNING|ERROR) (?P<message>.+)"
)

def parse_log(line):
    match = LOG_LINE.fullmatch(line)
    return match.groupdict() if match else None

print(parse_log("2026-08-22 ERROR timeout waiting for API"))
print(parse_log("ERROR timeout"))

fullmatch вимагає, щоб увесь рядок відповідав формату; groupdict() повертає зрозумілий mapping із named groups.

Очікуваний результат: Dict із date/level/message для першого рядка, потім None.

#regex#strings#parsing

Джерела

The Python Standard LibraryPython Software Foundation · Authoritative behavior for built-in types and standard library modules
Functions and functional toolsJuniorVery commonPractical

Виправте функцію, яка випадково ділить один список між викликами через `items=[]` як аргумент за замовчуванням.

Відповідь

Замініть змінюване значення за замовчуванням на `None` і створюйте новий список усередині функції, коли викликач його не передав. Вирази за замовчуванням виконуються один раз під час визначення функції, тому list literal у сигнатурі стає одним спільним об'єктом. Sentinel усуває цей спільний стан і все ще дозволяє навмисно передати власний список.

Сильна відповідь включає

  • використовує None як sentinel
  • створює список всередині функції
  • пояснює одноразове обчислення defaults при визначенні

Практичні приклади

None як sentinel
def collect(item, items=None):
    if items is None:
        items = []
    items.append(item)
    return items

print(collect("a"))
print(collect("b"))

Кожен виклик без `items` створює новий список під час виконання замість повторного використання одного об'єкта з defaults функції.

Очікуваний результат: ['a'], потім ['b'].

#functions#mutability#gotchas

Джерела

The Python TutorialPython Software Foundation · Baseline syntax, control flow, functions and module basicsThe Python Language ReferencePython Software Foundation · Precise scoping, evaluation order and expression semantics
Iterators, generators and decoratorsMiddleVery commonPractical

Напишіть retry-декоратор, що повторює виклик лише для вказаного типу exception і зберігає metadata обгорнутої функції.

Відповідь

Використайте decorator factory, щоб тип exception і кількість спроб були конфігурацією, а функцію обгорніть через `functools.wraps`. Перехоплюйте лише налаштований exception замість загального `Exception`, робіть обмежену кількість спроб і повторно кидайте останню помилку. Це не приховує сторонні programming errors і зберігає функцію придатною до introspection.

Сильна відповідь включає

  • використовує decorator factory і functools.wraps
  • перехоплює лише налаштований exception
  • обмежує кількість спроб і повторно кидає фінальну помилку

Практичні приклади

Обмежений retry decorator
from functools import wraps

def retry(exception_type, attempts=3):
    def decorate(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            for attempt in range(1, attempts + 1):
                try:
                    return func(*args, **kwargs)
                except exception_type:
                    if attempt == attempts:
                        raise
        return wrapper
    return decorate

calls = 0

@retry(TimeoutError, attempts=3)
def flaky():
    global calls
    calls += 1
    if calls < 3:
        raise TimeoutError("temporary")
    return "ok"

print(flaky())
print(calls)

Decorator повторює лише TimeoutError, зупиняється після трьох загальних спроб, а `@wraps` зберігає ім'я й metadata оригінальної функції.

Очікуваний результат: ok, потім 3.

#decorators#retry#functions

Джерела

functools — Higher-order functions and operations on callable objectsPython Software Foundation · lru_cache, partial, wraps, reduce and singledispatch behaviorErrors and ExceptionsPython Software Foundation · try/except/else/finally control flow and exception chaining
Iterators, generators and decoratorsMiddleCommonPractical

Напишіть generator, що повертає вхідну sequence блоками фіксованого розміру, не створюючи всі блоки наперед.

Відповідь

Для sequence, що підтримує slicing, ітеруйте стартові індекси з кроком розміру блока й робіть `yield` одного slice за раз. Оскільки функція використовує yield замість повернення повного списку slices, викликач може споживати кожен блок поступово. Перевірте, що розмір блока додатний, щоб некоректний input завершувався явно.

Сильна відповідь включає

  • використовує yield для лінивого створення блоків
  • рухає індекси з кроком chunk size
  • валідує додатний chunk size

Практичні приклади

Лінивий chunker sequence
def chunks(items, size):
    if size <= 0:
        raise ValueError("size must be positive")

    for start in range(0, len(items), size):
        yield items[start:start + size]

print(list(chunks([1, 2, 3, 4, 5], 2)))

Generator зберігає лише поточний execution state; кожен slice створюється, коли викликач просить наступний chunk.

Очікуваний результат: [[1, 2], [3, 4], [5]]

#generators#yield#algorithms

Джерела

The Python Language ReferencePython Software Foundation · Precise scoping, evaluation order and expression semanticsThe Python TutorialPython Software Foundation · Baseline syntax, control flow, functions and module basics
Errors and context managersMiddleCommonPractical

Реалізуйте context manager, що вимірює тривалість блока й усе одно записує час, якщо блок завершується exception.

Відповідь

Class-based context manager може зберегти `perf_counter()` у `__enter__` і обчислити elapsed time у `__exit__`. Оскільки `__exit__` виконується при виході з `with` навіть через exception, час фіксується і при успіху, і при помилці. Повернення `False` або `None` з `__exit__` дозволяє початковому exception поширитися далі.

Сильна відповідь включає

  • використовує __enter__ і __exit__
  • використовує monotonic high-resolution timer на кшталт perf_counter
  • не пригнічує exceptions випадково

Практичні приклади

Class-based timer
from time import perf_counter, sleep

class Timer:
    def __enter__(self):
        self.started = perf_counter()
        return self

    def __exit__(self, exc_type, exc, tb):
        self.elapsed = perf_counter() - self.started
        return False

with Timer() as timer:
    sleep(0.01)

print(timer.elapsed > 0)

Повернення timer з __enter__ робить його elapsed доступним після блока; False з __exit__ зберігає нормальне поширення exceptions.

Очікуваний результат: True.

#context-managers#timing#resource-safety

Джерела

Data modelPython Software Foundation · Dunder methods, object identity, attribute lookup and the descriptor and context manager protocolscontextlib — Utilities for with-statement contextsPython Software Foundation · Context manager helpers, including contextmanager and ExitStackThe Python Standard LibraryPython Software Foundation · Authoritative behavior for built-in types and standard library modules
Errors and context managersMiddleCommonPractical

Перетворіть низькорівневий `KeyError` на доменну configuration error, не втрачаючи початкову причину traceback.

Відповідь

Визначте доменний exception і перехоплюйте лише ту низькорівневу помилку, яку справді потрібно перекласти. Підніміть доменний exception через `from exc`, щоб Python записав початкову помилку як `__cause__`. Викликач отримує стабільний application-level exception, а debugging усе ще показує точний невдалий lookup ключа.

Сильна відповідь включає

  • визначає доменний exception
  • перехоплює лише потрібний низькорівневий exception
  • використовує raise ... from exc для chaining

Практичні приклади

Явний exception chaining
class ConfigError(RuntimeError):
    pass

def read_required(config, key):
    try:
        return config[key]
    except KeyError as exc:
        raise ConfigError(f"Missing required key: {key}") from exc

try:
    read_required({}, "token")
except ConfigError as exc:
    print(type(exc.__cause__).__name__)
    print(exc)

Викликач бачить доменний exception, а `__cause__` явно зберігає початковий KeyError.

Очікуваний результат: KeyError, потім Missing required key: token.

#exceptions#chaining#error-handling

Джерела

Errors and ExceptionsPython Software Foundation · try/except/else/finally control flow and exception chainingThe Python Language ReferencePython Software Foundation · Precise scoping, evaluation order and expression semantics
Pytest & fixturesMiddleVery commonPractical

Напишіть pytest fixture, що підключається до device перед тестом і завжди відключається після нього.

Відповідь

Використайте fixture з `yield`: setup виконується до `yield`, yield-об'єкт інжектиться в тест, а teardown продовжується після завершення тесту. Критичний teardown варто помістити в `finally`, коли cleanup має виконатися навіть якщо код після connect або саме тіло тесту завершується помилкою. Це стандартний pytest-патерн для ресурсів із lifecycle.

Сильна відповідь включає

  • використовує yield fixture
  • setup до yield, teardown після тесту
  • використовує finally для гарантованого cleanup за потреби

Практичні приклади

Yield fixture з гарантованим disconnect
import pytest

@pytest.fixture
def device():
    client = Device()
    client.connect()
    try:
        client.reset()
        yield client
    finally:
        client.disconnect()

def test_device_state(device):
    assert device.status() == "ready"

pytest зупиняє fixture на yield під час виконання тесту, а потім продовжує її. finally захищає disconnect, якщо reset, тест або наступний fixture-код падає після connection.

Очікуваний результат: Тест отримує підключений Device; disconnect() виконується після тесту, якщо connect() був успішним.

#pytest#fixtures#testing

Джерела

pytest documentationpytest · Fixture scope, parametrization, markers and plugin architecture
Pytest & fixturesJuniorVery commonPractical

Використайте pytest parametrization, щоб перевірити кілька валідних і невалідних age без дублювання тіла тесту.

Відповідь

Використайте `@pytest.mark.parametrize` з явними колонками input і expected result. pytest створює окремий test case для кожного рядка, тому failure вказує на конкретний набір даних, а assertion logic залишається в одному місці. Додайте boundary values, а не лише типові значення, бо саме на межах часто з'являються validation defects.

Сильна відповідь включає

  • використовує pytest.mark.parametrize
  • одне тіло тесту для кількох cases
  • включає boundary та invalid values

Практичні приклади

Parametrization із фокусом на boundaries
import pytest

def is_valid_age(age):
    return 18 <= age <= 120

@pytest.mark.parametrize(
    ("age", "expected"),
    [
        (17, False),
        (18, True),
        (120, True),
        (121, False),
    ],
)
def test_age_validation(age, expected):
    assert is_valid_age(age) is expected

Кожен tuple стає окремим pytest case, а вибрані дані покривають обидві валідні межі та сусідні невалідні значення.

Очікуваний результат: Чотири успішні test cases.

#pytest#parametrization#testing

Джерела

pytest documentationpytest · Fixture scope, parametrization, markers and plugin architecture
Python AQA reliabilityMiddleVery commonPractical

Створіть повторно використовуваний `requests.Session` з обмеженими retries для тимчасових GET-помилок, не повторюючи кожну можливу HTTP error.

Відповідь

Налаштуйте Session з HTTPAdapter на основі urllib3 `Retry`. Обмежте кількість retry, дозвольте їх для idempotent methods на кшталт GET, виберіть лише transient status codes — наприклад 429/502/503/504 — і окремо задайте request timeout, бо кількість retry не обмежує тривалість окремої спроби. Це дає явну, контрольовану resilience замість широкого retry loop навколо будь-якого exception.

Сильна відповідь включає

  • використовує Session плюс HTTPAdapter/Retry
  • повторює лише обмежені transient/idempotent cases
  • окремо задає per-request timeout

Практичні приклади

Session з явною transient retry policy
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry

retry = Retry(
    total=3,
    backoff_factor=0.25,
    status_forcelist=(429, 502, 503, 504),
    allowed_methods={"GET"},
)

session = requests.Session()
session.mount("https://", HTTPAdapter(max_retries=retry))

response = session.get("https://api.example.test/health", timeout=5)
response.raise_for_status()

Adapter контролює, які responses можна повторювати і скільки разів; timeout окремо обмежує кожну network attempt.

Очікуваний результат: Успішна response повертається нормально; вичерпані retries або фінальний error status завершуються стандартною requests/urllib3 помилкою.

#requests#http#retry#automation

Джерела

Requests: HTTP for HumansPython Software Foundation (Requests project) · Third-party HTTP client API, including Session-based connection pooling and configuration reuse
Web, automation and QA-relevant PythonMiddleVery commonPractical

Реалізуйте polling, що чекає, поки умова стане істинною, але ніколи не чекає безкінечно.

Відповідь

Використайте monotonic deadline на основі `time.monotonic()` замість підрахунку ітерацій чи порівняння wall-clock timestamps. Перевіряйте умову, повертайтесь одразу при успіху, робіть bounded sleep між спробами й піднімайте `TimeoutError` після дедлайну. Monotonic clock не стрибає назад або вперед через зміну системного часу.

Сильна відповідь включає

  • використовує time.monotonic для deadline
  • повертається одразу при успіху
  • викликає bounded timeout замість нескінченного циклу

Практичні приклади

Polling helper на основі deadline
from time import monotonic, sleep

def wait_until(check, timeout=5.0, interval=0.1):
    deadline = monotonic() + timeout

    while True:
        value = check()
        if value:
            return value
        if monotonic() >= deadline:
            raise TimeoutError("condition was not met")
        sleep(interval)

Абсолютний monotonic deadline тримає загальний час очікування обмеженим, навіть коли кількість ітерацій змінюється.

Очікуваний результат: Повертає перший truthy результат check або викликає TimeoutError приблизно після заданого timeout.

#polling#timeout#retry#automation

Джерела

The Python Standard LibraryPython Software Foundation · Authoritative behavior for built-in types and standard library modules
Concurrency and parallelismSeniorCommonPractical

Запустіть багато async operations конкурентно, але обмежте кількість операцій, що можуть бути активними одночасно.

Відповідь

Використайте `asyncio.Semaphore` навколо resource-intensive частини кожної coroutine, потім заплануйте всю роботу й дочекайтесь її через `asyncio.gather` або TaskGroup. Semaphore дає concurrency без необмеженого burst до API чи database. Тримайте scope `async with semaphore` якомога меншим, щоб непов'язана локальна робота не займала permit.

Сильна відповідь включає

  • використовує asyncio.Semaphore
  • запускає кілька coroutines конкурентно
  • обмежує лише resource-sensitive section

Практичні приклади

Обмеження concurrent async work через semaphore
import asyncio

async def fetch(name, semaphore):
    async with semaphore:
        await asyncio.sleep(0.05)
        return name

async def main():
    semaphore = asyncio.Semaphore(3)
    results = await asyncio.gather(
        *(fetch(str(i), semaphore) for i in range(10))
    )
    print(len(results))

asyncio.run(main())

Усі десять coroutines заплановані, але лише три можуть одночасно утримувати semaphore й виконувати захищений simulated I/O.

Очікуваний результат: 10.

#asyncio#semaphore#concurrency

Джерела

asyncio — Asynchronous I/OPython Software Foundation · Event loop, coroutines, tasks and structured concurrency primitivesPEP 492 — Coroutines with async and await syntaxPython Software Foundation · The origin of async/await syntax and native coroutine objects
Concurrency and parallelismMiddleCommonPractical

Зробіть спільний counter безпечним, коли кілька threads збільшують його конкурентно.

Відповідь

Захистіть read-modify-write оновлення одним спільним `threading.Lock`, бажано через `with lock:`, щоб release був exception-safe. Кожен thread, що читає або змінює захищений counter, має дотримуватись того самого правила синхронізації. GIL не є application-level lock і не повинен бути аргументом коректності для compound operations над shared state.

Сильна відповідь включає

  • використовує один спільний Lock
  • захищає compound counter update
  • не покладається на GIL для коректності

Практичні приклади

Lock критичної секції
from threading import Lock, Thread

counter = 0
lock = Lock()

def worker():
    global counter
    for _ in range(5_000):
        with lock:
            counter += 1

threads = [Thread(target=worker) for _ in range(4)]
for thread in threads:
    thread.start()
for thread in threads:
    thread.join()

print(counter)

Кожен increment виконується під тим самим lock, тому compound update не може переплестися з іншим захищеним increment.

Очікуваний результат: 20000.

#threading#locks#race-conditions

Джерела

threading — Thread-based parallelismPython Software Foundation · Locks, thread safety and thread-local statePython support for free threadingPython Software Foundation · The GIL, the free-threaded build and its compatibility trade-offs
Object-oriented PythonMiddleCommonPractical

Змоделюйте test result через dataclass і відхиляйте некоректні duration під час створення об'єкта.

Відповідь

Оголосіть data fields через `@dataclass`, а invariants, що залежать від ініціалізованих полів, перевіряйте в `__post_init__`. Так зберігаються автоматично згенеровані `__init__`, `__repr__` і equality behavior, але domain rules усе одно контролюються. Для негативного duration підніміть чіткий standard exception на кшталт `ValueError`.

Сильна відповідь включає

  • використовує @dataclass для record
  • валідує initialized fields у __post_init__
  • викликає зрозумілий exception для invalid input

Практичні приклади

Dataclass з invariant під час створення
from dataclasses import dataclass

@dataclass
class TestResult:
    name: str
    status: str
    duration: float

    def __post_init__(self):
        if self.duration < 0:
            raise ValueError("duration cannot be negative")

result = TestResult("login", "passed", 1.2)
print(result)

Dataclass генерує constructor і representation; __post_init__ виконується одразу після присвоєння полів і контролює domain rule.

Очікуваний результат: TestResult(name='login', status='passed', duration=1.2)

#dataclasses#oop#validation

Джерела

dataclasses — Data ClassesPython Software Foundation · Generated dunder methods, field defaults and frozen/eq/order semanticsErrors and ExceptionsPython Software Foundation · try/except/else/finally control flow and exception chaining