Package: typedi
typediSimple yet powerful typed annotation-based dependency injection container.To install from python package index simply type (no dependencies):pip install typediDependency InjectionDependency Injection (DI) - is a design pattern where an object automatically receives other objects that it depends on.
It is an approach to implement Inversion of Control - way to separate concerns and reduce coupling.To learn more I suggest readingInversion of Control Containers and the Dependency Injection patternby Martin Fowler.When DI in python is useful (my subjective opinion):Large modularized applicationsFrameworks foundationPlugin systemsWhen it is harmful or might bring unnecessary complexity:Small applicationsSimple elegant pythonic projectsLarge untyped, poorly structured code (will require lots of refactoring in a first place)typeditypedi- implements "constructor based" dependency injection by heavily utilizing python's type annotations.
This means that you specify object dependencies just by type annotations in__init__methods or by annotating dataclasses.From looking at object initializerstypedifigures out dependencies and tries its best to provide them:classAbstractService:passclassConcreteService(AbstractService):passclassApp:# App depends on AbstractService.# Such that we can provide multiple implementations, i.e. for testing purposes.def__init__(self,service:AbstractService):self.service=servicefromtypediimportContainercontainer=Container()# creating a DI container# Registering instance of a ConcreteService into container.container.register_instance(ConcreteService())# Registering App class.# Container will inspect the signature of __init__.container.register_class(App)# First, container tries to specify dependencies of App:#   * The container looks up registered providers for anything that matches AbstractService.#   * There is an instance of ConcreteService which matches AbstractService, so it is a match.# Then instance of App is created with a ConcreteService instance value for `service` argument.application=container.resolve(App)This example is unrealistically simple.
But it highlights the fundamental idea of specifying and resolving dependencies from type signatures.Here is more complex example highlighting some more features:fromtypingimportProtocol,List,runtime_checkablefromdataclassesimportdataclass# Note: we are using protocol# Abstract classes (abc) or basic inheritance would also work@runtime_checkableclassMathOperation(Protocol):defapply(self,a:int,b:int)->int:...classSumOperation:# Note: not even subclassingdefapply(self,a:int,b:int)->int:returna+bclassSubtractOperation:# Note: not even subclassingdefapply(self,a:int,b:int)->int:returna-b@dataclass# dataclasses (or even attrs) are supportedclassApp:# Require instances that support MathOperation Protocoloperations:List[MathOperation]# Note: typedi also supports dependencies specified using complex types:#   List, Iterable, Union, Optional, Tuple, Type and even Anydefdo_something(self,a:int,b:int):foropinself.operations:print(f"{op}:{op.apply(a,b)}")# Importing here just to highlight that it is not needed anywhere abovefromtypediimportContainercontainer=Container()# Register operations as singletons.# There will be at most 1 instance in the container# Instances will be created only when requested.container.register_singleton_class(SumOperation)container.register_singleton_class(SubtractOperation)# Registering App so that container will understand its dependencies.container.register_class(App)# Resolution happens here.app=container.resolve(App)app.do_something(3,4)typediaims 0 user code invasion - your code is yours:no magic meta subclassingno decoratorsno magicDependsdefault argumentsno magic__attributesYou could usetypediin existing application without altering existing class/factory code.ContainerThe central concept of most DI implementations includingtypediisContainer.
It serves two purposes:Stores registered dependency providersProvides interface to query constructed instancesTo create container:fromtypediimportContainercontainer=Container()Registering providersTo be able to resolve dependenciesContainershould know how to provide them,
what instances to provide, what functions to call or what classes to instantiate.typedisupports multiple different ways of registering instance providers.container.register_instance(SomeClass(...))- Container will just remember this instance and will provide it as a dependency for any object that requires typeSomeClass. This could be considered as container-scoped singletons that you manually created.container.register_factory(fn)- instead of providing complete initialized instance you could use a factory function that creates it. Container will look at the function signature and remember that instances of return type could be created by callingfn.
Every time a return type is requested container will callfnto create an instance.container.register_class(SomeClass)- same ascontainer.register_factorybut for classes. Container will remember that objects ofSomeClassor any parent type in class hierarchy could be created by instantiatingSomeClass.container.register_singleton_factory(fn)- same ascontainer.register_factorybut will be only called once per container and result will be cached.container.register_singleton_class(fn)- same ascontainer.register_classbut will be only called once per container and result will be cached.Note: it is possible to register multiple providers of the same type.Resolving instancesOnce container knows how to provide instances you can query them.
Objects are queried from the container by some queried type.container.resolve(T)- will return an single instance of typeT.container.get_all_instances(T)- will return a list of instances of typeT.container.iter_all_instances(T)- will return an iterable of instances of typeT.In each case container will try to find providers that can provide requested type and call them if needed and return resulting instances.If container is not able to provide an instance(s) of given type it raisesResolutionError.Supported providersHere is the list of things recognised by the Container. This means that you can register a factory or a class that produces type-> Tand container will be able to resolve it.SignatureDescriptionAsome class as a factory of its instancesA(...)any already created instancefn(...) -> Afactory of a single instancefn(...) -> Optional[T]factory of and optional instance of typeTfn(...) -> Union[T1,...,Tn]factory of either typesfn(...) -> List[T]factory of many instances of typeTfn(...) -> Iterable[T]factory of many instances ofT, might be a be a generator as well.fn(...) -> Tuple[T1, T2]factory of tuple ofT1andT2as well asT1andT1instances separately.fn(...) -> Anyfactory of any instances (types will be runtime checked).fn(...) -> Type[T]factory of generic meta type ofT.Ais any concrete class type.T- any supported type including concrete classes, or genericsOptional[T],Union[T1, T2],Iterable[T],List[T]andTuple[T1, ..., TN]. Which means you can go crazy and return something like-> Iterable[Optional[List[Union[A, B]]]]. Container will deal with it....- is valid annotated signature with 0 or more arguments with definitions, for example:arg: T- argument of some specific typeT. If container is not able to resolveTit will raiseResolutionError.arg: Optional[T]- if container will fail to resolveT,Nonevalue will be used.*args: T- varargs will be resolved asTuple[T]. If it is not possible to resolveT- emptytuple()will be passed.arg: T = default_value- container will try to resolveTbut if it fails,default_valuewill be used.Container will perform a.resolve()for each argument by its annotation.
For more information about resolution see below.Few examples of valid factory/class signatures that you can register:A()
f() -> A
f(b: B) -> A
f(*args: B) -> A
f(bs: List[B]) -> A
f(b: Optional[B]) -> A
f(bs: Iterable[B]) -> A
f(b: Optional[B] = None) -> A
f(*args: B, arg: C = DEFAULT_C) -> A
f() -> Tuple[List[A], B, C]
f() -> Union[A, B, C]
f() -> Tuple[A, B, C]
f() -> Iterable[A]
f(*args: A) -> Any
f(a: A) -> A
A(a: A)These could also be used as factories:functlools.partialof any typed callableCustom callable classes (with annotated__call__)@dataclass,@attrsNot supported return annotations:FinalLiteralAnnotatedgeneric annotations withTypeVarSupported resolution type queriesQuery (T)DescriptionAResolves an instance ofA, raisesResolutionErrorif not possibleOptional[T]Resolves an instance of typeTif possible, otherwise returnsNoneUnion[T1,...,Tn]Tries resolution of each type argument left to right, if nothing resolves raisesResolutionErrorList[T]Resolves all instances ofTand returns a list. If none ofTavailble, provides empty list.Iterable[T]Resolves all instances ofTinto an iterable. If non ofTavailable, provides empty iterable.Tuple[T1,...,Tn]First, tries to resolve an instance ofTuple[T1,...,Tn]as is. If it is not available provides instances ofT1toTnas a tuple.typedialso resolves covariant types and even covariant generics likeList,Tuple,Iterableand even higher orderType[T].Covariance example:fromtypingimportListclassBase:passclassConcrete(Base):passfromtypediimportContainercontainer=Container()# Registering concrete instancescontainer.register_instance(Concrete())container.register_instance(Concrete())# Query all instances that are instance of Baseconcrete_instances=container.resolve(List[Base])assertisinstance(concrete_instances[0],Concrete)assertisinstance(concrete_instances[1],Concrete)Featurestypedi also has support of various features:Factory functions (includingfunctools.partial)Singletons support, both for classes and factory functionsOptionals support - ability to implement "try resolve or return None if no dependency" behaviorIf you want to learn more, please refer to typedi_tests and actual implementation since it is quite self-describing :)Circular dependenciestypedican handle circular dependencies solving "chicken and egg" problem:classA:def__init__(self,b:"B"):self.b=bclassB:def__init__(self,a:A):self.a=afromtypediimportContainercontainer=Container()# Register both classescontainer.register_class(A)container.register_class(B)a=container.resolve(A)assertisinstance(a,A)assertisinstance(a.b,B)# Circular dependencyassertisinstance(a.b.a,A)asserta.b.a==aasserta.b.aisnota# NOTE: a.b.a is a proxy of a, not the exact instanceThis code actually works.
It is done internally by constructing a proxy object when encountering circular dependency during resolution.
The proxy is completely identical to the original. You can use proxies in the same way as the original objects.Proxy objects are used only to resolve a circular dependency. All non-circular dependencies would resolve in original, genuine objects.The only restriction is that it is impossible to access the proxied circular dependencies inside the factory method (or__init__) itself. This code will raise an error during resolution:...classB:def__init__(self,a:A):print(a)# access to method __str__ of not-yet initialized proxy instance of Aself.a=a...Performance considerationsAs you might have guessed already, there is a lot of introspection, reflection,type(...)andisinstance(...)going on under the hood when registering and resolving.
I do not recommend usingContainermethods in your runtime hot paths since.resolve()call is quite expensive and might affect performance.
Use it to assemble things on startup and throw theContaineraway.Containertries its best to reduce unnecessary calls to factories.
However, if return annotation of a provider is not strict enough (Union, orAny) in order to resolve some instance of that factory
there is no other way other than call it and just go through all the options and check if it does match the query.
Resolution complexity aligns in a way:ConcreteType < Union[ConcreteTypeA, ConcreteTypeB] < Any.Another costly part is generators, if you have a generator that produces instance of typesA,Band thenC,
resolving aCwould also result in instantiatingAandB:fromtypingimportUnion,IterableclassA:...classB:...classC:...defgenerator()->Iterable[Union[A,B,C]]:yieldA()# wasted callyieldB()# wasted callyieldC()yieldA()# also wasted call because container caches an entire result of a factoryfromtypediimportContainercontainer=Container()container.register_factory(generator)# generator would be called, and container will go through all generated instances# and check isinstance(item, C), once found, instance of C will be returnedc=container.resolve(C)assertisinstance(c,C)About python 3.6 and oldertypingmodule became more or less stable only after python 3.7 (including). API for types in 3.6 differs significantly from 3.7 onward.It is hard to maintain two different APIs that's why I decided to stick to the newer one.
However, if you feel brave enough and really need that 3.6 support you can open a PullRequest and contact me if you have any questions.TestingThis project is using tox (and pytest) to test among multiple python versions. To run test suites and generate coverage reports simply executetoxIf you don't have tox installed, executepip install toxfirst.