Terminology

At its heart, Injector is simply a dictionary for mapping types to things that create instances of those types. This could be as simple as:

{str: 'an instance of a string'}

For those new to dependency-injection and/or Guice, though, some of the terminology used may not be obvious.

Provider

A means of providing an instance of a type. Built-in providers include:

In order to create custom provider you need to subclass Provider and override its get() method.

Scope

By default, providers are executed each time an instance is required. Scopes allow this behaviour to be customised. For example, SingletonScope (typically used through the class decorator singleton), can be used to always provide the same instance of a class.

Other examples of where scopes might be a threading scope, where instances are provided per-thread, or a request scope, where instances are provided per-HTTP-request.

The default scope is NoScope.

See also

Scopes

Binding

A binding is the mapping of a unique binding key to a corresponding provider. For example:

>>> from injector import InstanceProvider
>>> bindings = {
...   (Name, None): InstanceProvider('Sherlock'),
...   (Description, None): InstanceProvider('A man of astounding insight'),
... }

Binder

The Binder is simply a convenient wrapper around the dictionary that maps types to providers. It provides methods that make declaring bindings easier.

Module

A Module configures bindings. It provides methods that simplify the process of binding a key to a provider. For example the above bindings would be created with:

>>> from injector import Module
>>> class MyModule(Module):
...     def configure(self, binder):
...         binder.bind(Name, to='Sherlock')
...         binder.bind(Description, to='A man of astounding insight')

For more complex instance construction, methods decorated with @provider will be called to resolve binding keys:

>>> from injector import provider
>>> class MyModule(Module):
...     def configure(self, binder):
...         binder.bind(Name, to='Sherlock')
...
...     @provider
...     def describe(self) -> Description:
...         return 'A man of astounding insight (at %s)' % time.time()

Injection

Injection is the process of providing an instance of a type, to a method that uses that instance. It is achieved with the inject decorator. Keyword arguments to inject define which arguments in its decorated method should be injected, and with what.

Here is an example of injection on a module provider method, and on the constructor of a normal class:

from typing import NewType

from injector import Binder, Module, inject, provider

Name = NewType("Name", str)
Description = NewType("Description", str)

class User:
    @inject
    def __init__(self, name: Name, description: Description):
        self.name = name
        self.description = description

class UserModule(Module):
    def configure(self, binder: Binder):
       binder.bind(User)

class UserAttributeModule(Module):
    def configure(self, binder: Binder):
        binder.bind(Name, to='Sherlock')

    @provider
    def describe(self, name: Name) -> Description:
        return '%s is a man of astounding insight' % name

Injector

The Injector brings everything together. It takes a list of Module s, and configures them with a binder, effectively creating a dependency graph:

from injector import Injector
injector = Injector([UserModule(), UserAttributeModule()])

You can also pass classes instead of instances to Injector, it will instantiate them for you:

injector = Injector([UserModule, UserAttributeModule])

The injector can then be used to acquire instances of a type, either directly:

>>> injector.get(Name)
'Sherlock'
>>> injector.get(Description)
'Sherlock is a man of astounding insight'

Or transitively:

>>> user = injector.get(User)
>>> isinstance(user, User)
True
>>> user.name
'Sherlock'
>>> user.description
'Sherlock is a man of astounding insight'

Assisted injection

Sometimes there are classes that have injectable and non-injectable parameters in their constructors. Let’s have for example:

class Database: pass


class User:
    def __init__(self, name):
        self.name = name


class UserUpdater:
    def __init__(self, db: Database, user):
        pass

You may want to have database connection db injected into UserUpdater constructor, but in the same time provide user object by yourself, and assuming that user object is a value object and there’s many users in your application it doesn’t make much sense to inject objects of class User.

In this situation there’s technique called Assisted injection:

from injector import ClassAssistedBuilder
injector = Injector()
builder = injector.get(ClassAssistedBuilder[UserUpdater])
user = User('John')
user_updater = builder.build(user=user)

This way we don’t get UserUpdater directly but rather a builder object. Such builder has build(**kwargs) method which takes non-injectable parameters, combines them with injectable dependencies of UserUpdater and calls UserUpdater initializer using all of them.

AssistedBuilder[T] and ClassAssistedBuilder[T] are injectable just as anything else, if you need instance of it you just ask for it like that:

class NeedsUserUpdater:
    @inject
    def __init__(self, builder: ClassAssistedBuilder[UserUpdater]):
        self.updater_builder = builder

    def method(self):
        updater = self.updater_builder.build(user=None)

ClassAssistedBuilder means it’ll construct a concrete class and no bindings will be used.

If you want to follow bindings and construct class pointed to by a key you use AssistedBuilder and can do it like this:

>>> DB = Key('DB')
>>> class DBImplementation:
...     def __init__(self, uri):
...         pass
...
>>> def configure(binder):
...     binder.bind(DB, to=DBImplementation)
...
>>> injector = Injector(configure)
>>> builder = injector.get(AssistedBuilder[DB])
>>> isinstance(builder.build(uri='x'), DBImplementation)
True

More information on this topic:

Child injectors

Concept similar to Guice’s child injectors is supported by Injector. This way you can have one injector that inherits bindings from other injector (parent) but these bindings can be overriden in it and it doesn’t affect parent injector bindings:

>>> def configure_parent(binder):
...     binder.bind(str, to='asd')
...     binder.bind(int, to=42)
...
>>> def configure_child(binder):
...     binder.bind(str, to='qwe')
...
>>> parent = Injector(configure_parent)
>>> child = parent.create_child_injector(configure_child)
>>> parent.get(str), parent.get(int)
('asd', 42)
>>> child.get(str), child.get(int)
('qwe', 42)

Note: Default scopes are bound only to root injector. Binding them manually to child injectors will result in unexpected behaviour. Note 2: Once a binding key is present in parent injector scope (like singleton scope), provider saved there takes predecence when binding is overridden in child injector in the same scope. This behaviour is subject to change:

>>> def configure_parent(binder):
...     binder.bind(str, to='asd', scope=singleton)
...
>>> def configure_child(binder):
...     binder.bind(str, to='qwe', scope=singleton)
...
>>> parent = Injector(configure_parent)
>>> child = parent.create_child_injector(configure_child)
>>> child.get(str) # this behaves as expected
'qwe'
>>> parent.get(str) # wat
'qwe'