Виртуальное окружение
У каждого проекта — своя копия Python со своими пакетами. Проекты не конфликтуют версиями библиотек, системный Python не засоряется.
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -r requirements.txt
Точные версии фиксируют lock-файлом — uv.lock, poetry.lock или хотя бы pip freeze, — иначе через полгода сборка соберёт другие версии. uv и poetry управляют окружением и зависимостями одной командой.
Как работает импорт
При первом import orders Python:
- ищет модуль по
sys.path; - выполняет его сверху вниз один раз;
- кладёт в
sys.modules— повторные импорты берут оттуда.
Отсюда два правила. На уровне модуля не делают тяжёлого — подключений к базе, сетевых запросов: это выполнится при импорте. И циклические импорты ломаются: если a импортирует b, а b берёт имя из ещё не выполненного до конца a, получится ImportError: cannot import name. Лечат разделением модулей, импортом модуля вместо имени или импортом внутри функции. Импорт только для аннотаций — под if TYPE_CHECKING:.
if __name__ == "__main__"
def main():
...
if __name__ == "__main__":
main()
При запуске python app.py у модуля __name__ равно "__main__", при импорте — его имя. Так один файл служит и модулем, и программой. Для multiprocessing со spawn (Windows, macOS) эта защита обязательна.
pytest
Тест — функция test_… в файле test_*.py, проверка — обычный assert: pytest сам покажет, какие значения не совпали.
import pytest
@pytest.mark.parametrize("price, discount, expected", [
(100, 0, 100),
(100, 0.1, 90),
])
def test_total(price, discount, expected):
assert total([price], discount) == expected
def test_negative_discount():
with pytest.raises(ValueError):
total([100], -1)
pytest -x останавливается на первой ошибке, -k имя выбирает тесты. Тест проверяет одно поведение и не зависит от порядка запуска.
Фикстуры
Фикстура — функция с @pytest.fixture, результат которой pytest передаёт в тест по имени параметра. Код после yield — очистка:
@pytest.fixture(scope="session")
def db():
conn = connect_test_db()
yield conn
conn.close()
def test_save(db):
...
scope задаёт, как часто фикстура создаётся заново: дорогое (контейнер базы) — раз на сессию, дешёвое и изменяемое — на каждый тест. Общие фикстуры кладут в conftest.py, их видно без импорта.
mock.patch — патчить там, где используют
patch временно заменяет атрибут по имени на Mock. Подменять нужно в модуле, где имя ищется, а не где оно определено:
# orders.py: from payments import charge
with patch("orders.charge") as charge: # не "payments.charge"
charge.return_value = {"ok": True}
place_order(...)
charge.assert_called_once()
autospec=True создаёт мок с сигнатурой оригинала — неверный вызов упадёт. Для async-функций — AsyncMock. Много моков в тесте — признак сильной связанности: часто лучше фейковая реализация или интеграционный тест.
Что запомнить
- У каждого проекта — свой venv и lock-файл.
- Импорт выполняет модуль — тяжёлое на уровне модуля не делают.
assert,parametrize,pytest.raises— основа тестов.- Фикстуры — по имени параметра, очистка после
yield. - patch — там, где имя используется.