Контекст запроса можно добавить во все логи без ручного extra
В ASGI-приложении запрос заканчивается не там, где начался: обработчик вызывает корутины из других слоёв, и каждая пишет собственные сообщения. Если добавить
Явное лучше неявного, но собирать один и тот же контекст в каждом слое и вручную передавать его дальше — дорогая трактовка PEP 20.
В разборе распространения контекста логов показано, как общие поля попадают в сообщения из разных слоёв ASGI-приложения без ручной передачи по всей цепочке вызовов.
В ASGI-приложении запрос заканчивается не там, где начался: обработчик вызывает корутины из других слоёв, и каждая пишет собственные сообщения. Если добавить
user_id и platform через extra только в обработчике, запись из вложенной функции останется без этих полей.Явное лучше неявного, но собирать один и тот же контекст в каждом слое и вручную передавать его дальше — дорогая трактовка PEP 20.
В разборе распространения контекста логов показано, как общие поля попадают в сообщения из разных слоёв ASGI-приложения без ручной передачи по всей цепочке вызовов.
Сигналы Django подводят в критичных сценариях, но получателей можно выполнять надёжнее
Сигналы Django удобно разрывают зависимости между модулями. В обработке платежа этого мало: способ доставки сигналов остаётся подвержен неожиданным сбоям. Слабая связанность полезна, но надёжность от неё не появляется.
Альтернативный способ запускает получателей как фоновые задачи Django Tasks. В материале решение проверяют на процессе создания платежа и заказа, связывают с транзакциями базы данных, моделируют сбои и отдельно рассматривают атомарность.
В статье Reliable Django Signals остались тесты надёжных сигналов, ограничения подхода и направления дальнейшей работы. Стоит прочитать до того, как доверить сигналам этап процесса, который нельзя потерять из-за неожиданного сбоя.
Сигналы Django удобно разрывают зависимости между модулями. В обработке платежа этого мало: способ доставки сигналов остаётся подвержен неожиданным сбоям. Слабая связанность полезна, но надёжность от неё не появляется.
Альтернативный способ запускает получателей как фоновые задачи Django Tasks. В материале решение проверяют на процессе создания платежа и заказа, связывают с транзакциями базы данных, моделируют сбои и отдельно рассматривают атомарность.
В статье Reliable Django Signals остались тесты надёжных сигналов, ограничения подхода и направления дальнейшей работы. Стоит прочитать до того, как доверить сигналам этап процесса, который нельзя потерять из-за неожиданного сбоя.
Hakibenita
Reliable Django Signals
Using background tasks to reliability execute signal receivers
🔥1
Все дела подождут, потому что сегодня ваш день!
Поздравляем с Днем программиста и желаем меньше багов и больше фич, меньше созвонов и больше фокуса, меньше тасок с пометкой «срочно» и больше времени на отдых.
Забирайте подарок из нашей IT-коробки. Мы уверены, этот презент точно пригодится вам в работе: https://tprg.ru/la2S
Поздравляем с Днем программиста и желаем меньше багов и больше фич, меньше созвонов и больше фокуса, меньше тасок с пометкой «срочно» и больше времени на отдых.
Забирайте подарок из нашей IT-коробки. Мы уверены, этот презент точно пригодится вам в работе: https://tprg.ru/la2S
❤5🥱1
Атрибуты модуля можно создавать только при обращении
Если часть API не нужна во время запуска, готовить её заранее необязательно. Начиная с Python 3.7, в модуле можно определить
Для нужного имени функция возвращает динамически созданное значение, для остальных поднимает
В разборе атрибутов модуля есть минимальный пример с обычной функцией, динамическим атрибутом и
Если часть API не нужна во время запуска, готовить её заранее необязательно. Начиная с Python 3.7, в модуле можно определить
__getattr__: он обрабатывает обращение к имени, которого в модуле нет.Для нужного имени функция возвращает динамически созданное значение, для остальных поднимает
AttributeError. Так создание атрибута переносится на момент, когда он действительно понадобился, а запуск не ждёт этой работы. Лень здесь вполне питонична, пока ошибка для неизвестных имён остаётся явной.В разборе атрибутов модуля есть минимальный пример с обычной функцией, динамическим атрибутом и
AttributeError для неизвестного имени.Когда isinstance() мало, TypeGuard может удивить
С
На этом месте интуиция может подвести: автор несколько раз отказывался от
Стоит прочитать перед следующим пользовательским предикатом: в статье осталось главное, почему
С
isinstance() всё просто: mypy сам сужает объединение типов в каждой ветке. Сложнее, если объект приходится распознавать по содержимому, например проверять поля словаря. Тогда проверку выносят в отдельный предикат, а TypeGuard сообщает анализатору, какой тип прошёл условие.На этом месте интуиция может подвести: автор несколько раз отказывался от
TypeGuard и заканчивал комментарием # type: ignore. В разборе TypeGuard и TypeIs он начинает с TypedDict для Person и объясняет, почему TypeIs соответствует ожиданиям лучше.Стоит прочитать перед следующим пользовательским предикатом: в статье осталось главное, почему
TypeIs оказался тем интерфейсом, которого автор ожидал от TypeGuard.Потоки ускоряют ввод-вывод, пока мелкая работа не упирается в процессор
Вы увеличиваете число рабочих потоков
Перейти на
В разборе ProcessThreadPoolExecutor остались возврат результатов через собственные объекты
Вы увеличиваете число рабочих потоков
ThreadPoolExecutor, настраиваете пул соединений, а пропускная способность перестаёт расти. Даже короткий вычислительный участок повторяется в каждом задании и постепенно отбирает выигрыш от новых потоков.Перейти на
ProcessPoolExecutor можно, но тогда вход приходится разбивать на пачки, менять код и платить памятью за процессы. Автор собирает гибрид: несколько процессов, внутри каждого пул потоков, с привычным интерфейсом исполнителя задач.В разборе ProcessThreadPoolExecutor остались возврат результатов через собственные объекты
Future, гибель рабочего процесса и влияние свободнопоточного Python на всю конструкцию.death and gravity
ProcessThreadPoolExecutor: when I/O becomes CPU-bound
...in which we build a hybrid concurrent.futures executor that runs I/O bound tasks on all available CPUs, thus evading the limitations imposed by the dreaded global interpreter lock on the humble ThreadPoolExecutor.
asyncio учат до первого исключения внутри задачи, а дальше начинается импровизация
Асинхронный код в проекте обычно дорастает до пары корутин и gather, а всё, что дальше, каждый достраивает по памяти: где ловить ошибку из задачи, упавшей молча, чем связывать корутины, кроме передачи результата в аргументах.
У Real Python есть разбор asyncio целиком, от корутин до выбора задач под них. Что в нём помимо привычного async/await:
• чем asyncio REPL (python -m asyncio) удобнее обёртки asyncio.run вокруг каждого эксперимента;
• как корутины стыкуются через asyncio.Queue, а не через цепочку await;
• что происходит с исключением, которое всплыло внутри задачи;
• в каких задачах asyncio выигрывает, а в каких только добавляет слой.
Открывать стоит ради последних двух пунктов: остальное вы, скорее всего, уже пишете руками.
#python
Асинхронный код в проекте обычно дорастает до пары корутин и gather, а всё, что дальше, каждый достраивает по памяти: где ловить ошибку из задачи, упавшей молча, чем связывать корутины, кроме передачи результата в аргументах.
У Real Python есть разбор asyncio целиком, от корутин до выбора задач под них. Что в нём помимо привычного async/await:
• чем asyncio REPL (python -m asyncio) удобнее обёртки asyncio.run вокруг каждого эксперимента;
• как корутины стыкуются через asyncio.Queue, а не через цепочку await;
• что происходит с исключением, которое всплыло внутри задачи;
• в каких задачах asyncio выигрывает, а в каких только добавляет слой.
Открывать стоит ради последних двух пунктов: остальное вы, скорее всего, уже пишете руками.
#python
Media is too big
VIEW IN TELEGRAM
Ко Дню программиста AvitoTech приготовил кое-что особенное!
Совместно со студией FU2RE и 3D-художником Dmitriev Video команда запустила большой портал в прошлое — эмулятор 2006 года будущего разработчика. Внутри олдскульные игры и викторины, за прохождение которых начисляются баллы, поднимающие позицию в рейтинге.
15 сентября трёх лучших игроков ждёт суперпак настоящих разрабов: салфетка на монитор из коллаборации с Elnik, плед, сумка и плюшевый талисман — кот Б/У. Так что успеть сыграть ещё можно!
P. S. А в канале AvitoTech до 13 сентября каждый день выходили праздничные посты. Присмотритесь, возможно, подарки спрятаны и в них 😉
Совместно со студией FU2RE и 3D-художником Dmitriev Video команда запустила большой портал в прошлое — эмулятор 2006 года будущего разработчика. Внутри олдскульные игры и викторины, за прохождение которых начисляются баллы, поднимающие позицию в рейтинге.
15 сентября трёх лучших игроков ждёт суперпак настоящих разрабов: салфетка на монитор из коллаборации с Elnik, плед, сумка и плюшевый талисман — кот Б/У. Так что успеть сыграть ещё можно!
P. S. А в канале AvitoTech до 13 сентября каждый день выходили праздничные посты. Присмотритесь, возможно, подарки спрятаны и в них 😉
👏1
Как вызывать асинхронный Python из синхронного кода
Если оборачивать каждый вызов в
С Python 3.11
В подробном разборе автор собирает
Если оборачивать каждый вызов в
asyncio.run(), Python каждый раз создаёт и закрывает цикл событий. Для одиночного запуска это нормально, но между вызовами не сохранятся привязанные к циклу ресурсы, например aiohttp.ClientSession с пулом соединений.С Python 3.11
asyncio.Runner позволяет выполнять несколько корутин в одном цикле. Однако пока цикл занят, синхронная часть приложения ждёт. Следующий шаг: перенести цикл в отдельный поток и передавать ему корутины из обычных функций.В подробном разборе автор собирает
ThreadRunner, который запускает корутины и превращает асинхронные итераторы в обычные. Получается точечный мост для существующего синхронного приложения без переписывания всей цепочки в async def.death and gravity
Running async code from sync code in Python
So, you're doing some sync stuff. But you also need to do some async stuff, without making *everything* async. Hint: asyncio.Runner will get you at least part of the way there.
❤1
Как ускорить сериализацию в Django Rest Framework
В бенчмарке на Python 3.7, Django 2.1.1 и DRF 3.9.4 объект User сериализовали 5000 раз без обращений к БД. ModelSerializer потратил 12,8 с, а функция справилась за 0,034 с. Разница составила 377 раз.
Профилировщик показал цену автоматизации: ModelSerializer заново строил поля из метаданных модели и готовил проверки. Режим только для чтения сократил время до 7,4 с, а Serializer с явно объявленными полями уложился в 2,1 с. Явное оказалось не только лучше, но и быстрее.
Для нагруженного API отделите время запроса к БД от сериализации. Если тормозит последняя, сделайте выходные поля доступными только для чтения или замените ModelSerializer на Serializer; для предельной скорости сравните с обычной функцией. Код и профили cProfile есть в разборе производительности DRF.
В бенчмарке на Python 3.7, Django 2.1.1 и DRF 3.9.4 объект User сериализовали 5000 раз без обращений к БД. ModelSerializer потратил 12,8 с, а функция справилась за 0,034 с. Разница составила 377 раз.
Профилировщик показал цену автоматизации: ModelSerializer заново строил поля из метаданных модели и готовил проверки. Режим только для чтения сократил время до 7,4 с, а Serializer с явно объявленными полями уложился в 2,1 с. Явное оказалось не только лучше, но и быстрее.
Для нагруженного API отделите время запроса к БД от сериализации. Если тормозит последняя, сделайте выходные поля доступными только для чтения или замените ModelSerializer на Serializer; для предельной скорости сравните с обычной функцией. Код и профили cProfile есть в разборе производительности DRF.
🔥1
Как тестировать HTTP-запросы в Python без хрупких моков
У асинхронного запроса через HTTPx есть четыре способа проверки. Можно вручную подменить
Библиотека respx перехватывает запросы HTTPx и возвращает заготовленный ответ. Код короче, зато тест остаётся привязан к HTTPx. Более явный вариант в духе Python: передать клиент в функцию и на тесте заменить его объектом-заглушкой.
Для интеграционной проверки приложение Starlette играет роль тестового сервера, а
У асинхронного запроса через HTTPx есть четыре способа проверки. Можно вручную подменить
AsyncClient.post, настроить асинхронный мок и проверить аргументы. С усложнением запроса растёт и эта обвязка.Библиотека respx перехватывает запросы HTTPx и возвращает заготовленный ответ. Код короче, зато тест остаётся привязан к HTTPx. Более явный вариант в духе Python: передать клиент в функцию и на тесте заменить его объектом-заглушкой.
Для интеграционной проверки приложение Starlette играет роль тестового сервера, а
AsyncClient обращается к нему вместо внешней сети. В статье есть код всех четырёх вариантов. Выбирайте respx для компактной подмены, заглушку вместо сторонней библиотеки для моков, тестовый сервер для проверки связки целиком.This media is not supported in your browser
VIEW IN TELEGRAM
Есть одна странная вещь в AI-индустрии.
Сегодня лучшая модель для кода — одна. Завтра выходит другая. Через месяц рынок снова меняется. Но большинство разработчиков всё ещё строят инфраструктуру так, будто им нужно выбрать одного AI-провайдера навсегда.
Polza.ai работает наоборот. Вы подключаете один API и получаете доступ к сотням моделей — OpenAI, Claude, Gemini, DeepSeek, Qwen и другим.
Нужна другая модель? Меняете её в запросе — и продолжаете работать. Можно сравнить провайдеров и цены и выбрать оптимальный вариант под конкретную задачу. А если один провайдер перестал отвечать, можно включить автомаршрутизацию — запрос автоматически уйдёт к другому.
Один баланс, одна интеграция, оплата в рублях. А в кабинете прозрачно видно всё по расходам: модель, провайдер, стоимость, количество токенов и статус каждого запроса. Расходы можно отслеживать в реальном времени.
В итоге вам не нужно угадывать, какая модель победит. Вы сами выбираете, что лучше подходит для конкретной задачи, и не привязываетесь к одному провайдеру.
Попробовать Polza.ai.
Сегодня лучшая модель для кода — одна. Завтра выходит другая. Через месяц рынок снова меняется. Но большинство разработчиков всё ещё строят инфраструктуру так, будто им нужно выбрать одного AI-провайдера навсегда.
Polza.ai работает наоборот. Вы подключаете один API и получаете доступ к сотням моделей — OpenAI, Claude, Gemini, DeepSeek, Qwen и другим.
Нужна другая модель? Меняете её в запросе — и продолжаете работать. Можно сравнить провайдеров и цены и выбрать оптимальный вариант под конкретную задачу. А если один провайдер перестал отвечать, можно включить автомаршрутизацию — запрос автоматически уйдёт к другому.
Один баланс, одна интеграция, оплата в рублях. А в кабинете прозрачно видно всё по расходам: модель, провайдер, стоимость, количество токенов и статус каждого запроса. Расходы можно отслеживать в реальном времени.
В итоге вам не нужно угадывать, какая модель победит. Вы сами выбираете, что лучше подходит для конкретной задачи, и не привязываетесь к одному провайдеру.
Попробовать Polza.ai.
😴2
Как собрать LRU-кеш с приоритетами и сроком жизни на Python
Обычный LRU-кеш удаляет давно не использовавшиеся записи. Здесь порядок сложнее: сначала просроченные элементы, потом записи с меньшим приоритетом, при равенстве — давно не запрашиваемые.
Автор начинает со словаря с операциями в среднем за O(1) и добавляет структуры для срока жизни, приоритета и истории обращений. Наивная очередь хранится в отсортированном списке: минимум легко прочитать, но вставка и удаление с начала требуют линейного времени. Затем очередь ускоряют через
Разбор Адриана на death and gravity показывает, как согласовать несколько структур данных и проверить сроки через внедряемые часы. Это пример разработки от простого рабочего варианта к более быстрому только на стандартной библиотеке.
Обычный LRU-кеш удаляет давно не использовавшиеся записи. Здесь порядок сложнее: сначала просроченные элементы, потом записи с меньшим приоритетом, при равенстве — давно не запрашиваемые.
Автор начинает со словаря с операциями в среднем за O(1) и добавляет структуры для срока жизни, приоритета и истории обращений. Наивная очередь хранится в отсортированном списке: минимум легко прочитать, но вставка и удаление с начала требуют линейного времени. Затем очередь ускоряют через
bisect, без куч и деревьев.Разбор Адриана на death and gravity показывает, как согласовать несколько структур данных и проверить сроки через внедряемые часы. Это пример разработки от простого рабочего варианта к более быстрому только на стандартной библиотеке.
death and gravity
This is not interview advice: a priority-expiry LRU cache in Python without heaps or trees
Today we're implementing a least recently used cache with priorities and expiry, using only the Python standard library.
This is a bIG TEch CoDINg InTerVIEW problem, so we'll work hard to stay away from the correct™ data structures, but we'll end up with…
This is a bIG TEch CoDINg InTerVIEW problem, so we'll work hard to stay away from the correct™ data structures, but we'll end up with…
Как ждать корутины в asyncio
Два последовательных
У каждого способа свои гарантии.
В статье Waiting in asyncio автор советует начинать с
Два последовательных
await не дают конкурентности: вторая корутина запустится после первой. Создание двух объектов корутин тоже не планирует их выполнение. Для этого нужны задачи, созданные до первого ожидания.У каждого способа свои гарантии.
gather() возвращает результаты в порядке аргументов. Если одна задача выбрасывает исключение, оно передаётся вызывающему коду сразу, а остальные задачи продолжают работать. as_completed() отдаёт результаты по мере готовности. wait() делит задачи на завершённые и ожидающие, а его тайм-аут сам ничего не отменяет.В статье Waiting in asyncio автор советует начинать с
TaskGroup, а для более гибкого управления выбирать wait(). Здесь «явное лучше неявного» работает буквально.Как сделать параметризованные тесты pytest читаемыми
Обычный список кортежей в
В статье Taming parametrize with pytest.param показан и следующий шаг: собрать именованные аргументы и ожидаемый результат в одном словаре. Такой вариант стоит примерить на тестах с длинной сигнатурой: у каждого сценария данные, имя и условия запуска оказываются рядом.
Обычный список кортежей в
@pytest.mark.parametrize быстро усложняет чтение: значения приходится сопоставлять с аргументами по позиции, а идентификаторы сценариев хранятся в отдельном списке.pytest.param помещает id рядом с данными. Тогда отдельный сценарий можно запустить командой pytest -k positive_x_axis. Через marks там же задаются xfail для ожидаемого сбоя и skipif для условного пропуска. Явное лучше неявного, особенно когда тест падает выборочно.В статье Taming parametrize with pytest.param показан и следующий шаг: собрать именованные аргументы и ожидаемый результат в одном словаре. Такой вариант стоит примерить на тестах с длинной сигнатурой: у каждого сценария данные, имя и условия запуска оказываются рядом.
Как быстрее загружать грязные данные в PostgreSQL из Python
Автор берёт постраничный API с данными о пиве, превращает его в генератор и перед записью нормализует поля: извлекает объём из вложенного объекта, а месяц и год приводит к дате. Для замеров 325 записей дублируются 100 раз, получается набор из 32 500 строк.
Затем импорт сравнивается через построчные INSERT, executemany, execute_batch, execute_values и COPY. Для пакетных вариантов с итераторами меняется размер страницы, для COPY из строкового итератора — размер буфера. У каждого подхода измеряются время и пиковая память. Явное лучше неявного, особенно когда скорость не приходится угадывать.
В подробном разборе есть код генератора, преобразований, профилировщика и всех вариантов загрузки. Его удобно использовать как основу замера на реальных данных.
Автор берёт постраничный API с данными о пиве, превращает его в генератор и перед записью нормализует поля: извлекает объём из вложенного объекта, а месяц и год приводит к дате. Для замеров 325 записей дублируются 100 раз, получается набор из 32 500 строк.
Затем импорт сравнивается через построчные INSERT, executemany, execute_batch, execute_values и COPY. Для пакетных вариантов с итераторами меняется размер страницы, для COPY из строкового итератора — размер буфера. У каждого подхода измеряются время и пиковая память. Явное лучше неявного, особенно когда скорость не приходится угадывать.
В подробном разборе есть код генератора, преобразований, профилировщика и всех вариантов загрузки. Его удобно использовать как основу замера на реальных данных.
❤1
Наследование, композиция или функции: сравниваем на Executor в Python
Статья сравнивает три реализации гибридного исполнителя задач: наследование, композицию и функции. Он разносит работу по процессам, а внутри каждого запускает потоки, чтобы I/O-нагрузка использовала все ядра, когда упирается в CPU.
Наследник
В разборе решения сравниваются по совместимости с будущими версиями, глобальному состоянию, сложности и отладке. Для полноценной замены считайте не только связи между классами, но и методы, которые придётся делегировать вручную.
Статья сравнивает три реализации гибридного исполнителя задач: наследование, композицию и функции. Он разносит работу по процессам, а внутри каждого запускает потоки, чтобы I/O-нагрузка использовала все ядра, когда упирается в CPU.
Наследник
ProcessPoolExecutor переопределяет __init__, submit() и shutdown(), а map() и контекстный менеджер получает от родителя. При композиции map() приходится копировать для вызова нового submit(), а контекстный протокол писать заново. «Явное лучше неявного» внезапно означает больше кода.В разборе решения сравниваются по совместимости с будущими версиями, глобальному состоянию, сложности и отладке. Для полноценной замены считайте не только связи между классами, но и методы, которые придётся делегировать вручную.
death and gravity
Inheritance over composition, sometimes
Last time, we built a hybrid concurrent.futures executor using inheritance. Today, we're building it again (twice!) using composition and functions only, to figure out which way is better and why. Consider this a worked example.
👍1
Почему mock.patch не подменяет функцию в Python
После
Практическое правило: подменяйте имя там, где его ищет тестируемый код. Здесь нужен
После
from os import listdir имя listdir привязано в модуле product. Последующая подмена os.listdir эту привязку не меняет, поэтому код продолжает вызывать product.listdir.mock.patch работает как временное присваивание: заменяет имя по указанному пути, а после блока with возвращает исходное значение.Практическое правило: подменяйте имя там, где его ищет тестируемый код. Здесь нужен
mock.patch("product.listdir"). Он изменит поведение только product.py, не затрагивая другие модули. В статье Why your mock doesn’t work механизм разобран на двух способах импорта и схемах связей между именами.❤1
Forwarded from Сохранёнки программиста
Как устроен асинхронный веб-краулер на Python
Обстоятельная глава строит краулер с нуля: от корневого URL он скачивает страницы, извлекает новые ссылки и ставит их в очередь. Число одновременных запросов ограничивают, чтобы избыток конкуренции не снижал производительность.
Разбор идёт от цикла событий с неблокирующими сокетами и колбэками к корутинам на генераторах, а затем к asyncio с асинхронной очередью. Переходы показывают, как избежать неуправляемых цепочек колбэков.
Асинхронность не означает параллельные вычисления и не обязана быть быстрее потоков. Она подходит для множества медленных соединений с редкими событиями, где поток на каждый запрос расходует память и упирается в системные ограничения.
В главе «A Web Crawler With asyncio Coroutines» из 500 Lines or Less код написан для Python 3.4. Читайте ради механики цикла событий; перед копированием сверяйте API с документацией.
Обстоятельная глава строит краулер с нуля: от корневого URL он скачивает страницы, извлекает новые ссылки и ставит их в очередь. Число одновременных запросов ограничивают, чтобы избыток конкуренции не снижал производительность.
Разбор идёт от цикла событий с неблокирующими сокетами и колбэками к корутинам на генераторах, а затем к asyncio с асинхронной очередью. Переходы показывают, как избежать неуправляемых цепочек колбэков.
Асинхронность не означает параллельные вычисления и не обязана быть быстрее потоков. Она подходит для множества медленных соединений с редкими событиями, где поток на каждый запрос расходует память и упирается в системные ограничения.
В главе «A Web Crawler With asyncio Coroutines» из 500 Lines or Less код написан для Python 3.4. Читайте ради механики цикла событий; перед копированием сверяйте API с документацией.
Как изолировать Pydantic Settings от окружения в pytest
Если
Решение автора: фикстура сохраняет
Подход пригодится, когда
Если
Settings создаётся при импорте пакета, он читает .env и переменные оболочки до запуска фикстуры pytest. Подмена os.environ запаздывает: значения уже сохранены в объекте, а тест зависит от окружения.Решение автора: фикстура сохраняет
model_copy() объекта, сбрасывает поля к значениям по умолчанию из model_fields и применяет точечные переопределения. Неизвестные настройки и неверные типы она отклоняет, после yield восстанавливает исходное состояние.Подход пригодится, когда
Settings уже живёт на уровне модуля. Явное снова лучше неявного. Код фикстуры и тесты со значениями по умолчанию и частичными переопределениями смотрите в разборе подмены Pydantic Settings.