2025, Oct 02 03:17

NewType поверх tuple[str, str] в Python injector: что ломается и как обойти

Injector в Python падает с UnknownProvider при привязке NewType(tuple[str, str]). Объясняем роль GenericAlias и даем обход через TYPE_CHECKING для DI.

В Python внедрение зависимостей часто опирается на типы времени выполнения, тогда как аннотации благодаря typing становятся всё выразительнее. Трение возникает, когда эти миры соприкасаются. Типичный случай: привязка NewType на основе параметризованного обобщённого типа вроде tuple[str, str] в injector падает во время выполнения с UnknownProvider.

Сбойная конфигурация

Следующий минимальный пример связывает NewType, оборачивающий типизированный кортеж, а затем пытается его разрешить:

import injector
from typing import NewType

PairLabel = NewType("PairLabel", tuple[str, str])


class AppModule(injector.Module):
    def configure(self, link: injector.Binder) -> None:
        link.bind(PairLabel, PairLabel(("x", "y")))


injector.Injector(modules=[AppModule]).get(PairLabel)

Во время выполнения это приводит к injector.UnknownProvider с сообщением о том, что нельзя определить провайдера для пары «интерфейс—экземпляр». Это лишь симптом; причина кроется глубже — в том, как Python представляет параметризованные обобщения на рантайме.

Что на самом деле ломается

Дело не в NewType. Важно то, что параметризованные обобщения вроде tuple[str, str] — это не реальные типы времени выполнения. Они являются экземплярами types.GenericAlias. Их нельзя применять там, где ожидается конкретный тип; даже isinstance откажется принимать их вторым аргументом. Разница очевидна:

>>> type(tuple[str, str])
<class 'types.GenericAlias'>
>>> isinstance(tuple[str, str], type)
False
>>> isinstance(tuple, type)
True
>>> isinstance(("foo", "bar"), tuple[str, str])
Traceback (most recent call last):
  ...
TypeError: isinstance() argument 2 cannot be a parameterized generic

Иными словами, во время выполнения у вас никогда не бывает экземпляра tuple[str, str]. Это лишь статическая конструкция типа. Поэтому попытка внедрить его не имеет смысла для DI‑фреймворка, который работает с реальными типами и объектами рантайма. А NewType на основе конкретного типа, например tuple или str, — это не проблема, поскольку это настоящие типы времени выполнения; по той же причине сработает и NewType('Baz', tuple).

Практическое обходное решение

Подход, который снимает блокировку, — сохранить точные типы для статической проверки, а рантайму дать конкретный тип для привязки. typing.TYPE_CHECKING позволяет развести определения для двух миров, не затрагивая исполняемую программу.

import injector
from typing import NewType, TYPE_CHECKING, reveal_type

if TYPE_CHECKING:
    PairLabel = NewType("PairLabel", tuple[str, str])
else:
    PairLabel = NewType("PairLabel", tuple)


class AppModule(injector.Module):
    def configure(self, link: injector.Binder) -> None:
        link.bind(PairLabel, PairLabel(("x", "y")))

reveal_type(injector.Injector(modules=[AppModule]).get(PairLabel))

С таким подходом статический анализ видит tuple[str, str] (или оборачивающий его NewType), тогда как рантайм получает конкретный tuple. Контейнер injector корректно разрешает привязку, потому что работает с реальным типом.

Почему это важно

Контейнеры внедрения зависимостей разрешают интерфейсы и ключи, опираясь на то, что существует во время выполнения. Параметризованные обобщения сводятся к GenericAlias и не могут служить конкретными типами рантайма. Понимание этой границы позволяет сохранять пользу от подсказок типов, не протаскивая чисто статические конструкции в проводку DI. Это также объясняет, почему NewType, оборачивающий str, работает хорошо в сценариях инъекции, а NewType, оборачивающий tuple[str, str], — нет.

Выводы

Привязывая значения для внедрения, придерживайтесь конкретных типов рантайма. Если вам нужна точность tuple[str, str] ради инструментов и качества кода, объявляйте свой NewType в блоке if TYPE_CHECKING, чтобы анализатор видел tuple[str, str], а рантайм — обычный tuple. Это устраивает injector и сохраняет статические гарантии там, где им место.

Статья основана на вопросе с StackOverflow от zyxue и ответе от Anerdw.