A Python dependency injection container that automatically resolves and injects dependencies without polluting your code with framework-specific decorators. Inspired by Lagom, Svcs, and C# .NET DI, it keeps your code clean and flexible while offering multiple service registration options. 🚀
Project description
Dependency injection without hands in Python.
Handless is a Python dependency injection container which aims at facilitating creation of your objects and services without polluting your code with framework specific code.
In particular it contains the following features:
- 🔌 Autowiring: Handless reads your objects constructor to determines its dependencies and resolve them automatically for you without explicit registration
- ♻️ Lifetimes: Handless allows you to pick between singleton, contextual, and transient lifetimes to determines when to reuse cached object or get new ones
- 🧹 Context managers: Handless automatically enter and exit context managers of your objects without having you to manage them
- 🔁 Inversion of control: Handless allows you to alias protocols or abstract classes to concrete implementations
- 🧠 Fully typed: Handless uses types for registrations. It makes sure that you register only things compatible with provided types and resolves objects with correct type
- 🧰 Flexible: Handless allows you to provide constant values, factories or lambda functions when registering your types
The following features are not available yet but planned:
- Async support: Handless will support async functions and context managers
- Positional only argument: Handless for the moment can not autowire function or constructors using positional only arguments
- Default values: Handless will use default values of functions or types arguments when its missing type annotation or nothing is registered for this type
- Change default lifetime: Handless will allow you to specify the default lifetime to use any registered type to fit your needs and reduce boilerplate
- Partial binding: Handless will provide ability to partially bind a function to a container allowing to execute that function and have the container resolve and inject its arguments on the fly
- Pings: Handless will allow you to register callbacks for your types allowing you to implement pings/health checks for objects interacting with shared resources (api, databases, ...
- Captive dependencies detection: Handless will try to provide ability to detect captive dependencies due to lifetimes mismatches during registration (e.g: a singleton type depending on a transient one)
Table of Content
Explanations
🔧 What is Dependency Injection, and Why Should You Care?
In modern software design, dependency injection (DI) is a technique where a component’s dependencies are provided from the outside, rather than hard-coded inside it. This leads to:
- ✅ More modular and testable code
- ✅ Easier substitution of dependencies (e.g., mocks, stubs, alternative implementations)
- ✅ Clearer separation of concerns
Trivial example without DI
class Service:
def __init__(self):
self.db = Database() # tightly coupled
Same exemple with DI
class Service:
def __init__(self, db: Database):
self.db = db # dependency injected
Doing dependency injection push creation and composition of your objects upfront. The place where you're doing this is called the composition root and is close to your application entrypoint(s).
:bulb: Your application can have many entrypoint and then many composition root, a CLI, a HTTP server, an event listener, ... Note that tests are also considered as entrypoints.
Doing dependency injection does not require any framework nor libraries, it can be achieved by "hand" (hence the name of this library "handless") by simply creating and composing your objects as expected. Doing so is called Pure DI.
However, manually composing your objects can be challenging in complex applications, in particular when you have to manage objects with different lifetimes (one per application, one per request, and so on...). It can also be complicated to compose only parts of your object graph with some objects replaced for testing purposes or for a different entrypoint (i.e: reusing some parts of your composition logic).
:warning: Using a dependency injection container is not mandatory. In simple applications it can be easier to do it manually. Always consider pros and cons.
This is where dependency injection containers can help you.
🧱 What is a DI Container?
As your project grows, wiring up dependencies manually becomes tedious and error-prone.
A dependency injection container role is to register once how to create and compose each of your objects in order to get instances of them on demand. The act of asking a container to get an instance of a specific type is called resolve. Finally, when you don't need those instances anymore you or the container will delete them and eventually do some cleanup (if specified). This last step is known as release.
Dependency injection containers can also:
- 🔍 Scan constructor signatures or factory functions
- 🔗 Resolve and injecting required dependencies
- ♻️ Manage object lifetimes (singleton, transient, scoped...)
- 🧹 Handle cleanup for context-managed resources
Instead of writing all the wiring logic yourself, the container does it for you — predictably and declaratively.
🚀 What This Library Solves
As stated in the introduction Handless provides you a dependency injection container that allows you to register your types and how to resolve them. It also takes care of lifetimes, context managers and is fully typed. Handless is able to read your types __init__ method to determine the dependencies to inject in order to create instances.
All of this is does not require you to add any library specific decorators or attributes to your existing types.
Its API provide lot of flexibility for registering your types.
This library provides a lightweight, flexible dependency injection container for Python that helps you:
- ✅ Register services with factories, values, aliases or constructors
- ✅ Resolve dependencies automatically (with type hints or custom logic)
- ✅ Manage lifecycles — including context-aware caching and cleanup (singleton, transient, contextual)
- ✅ Handle context managers by entering and exiting created objects context managers automatically
- And more...
It’s designed to be explicit, flexible, and intuitive
🧩 Design
Here are the main concept and design choice of Handless.
Container
Handless provides a handless.Container dependency injection container. This container allows you to register Python types and define how to resolve them. You can provide a function responsible of returning an instance of the type, a constant value, an alias (i.e: another type that should be resolved instead) or using the type constructor itself. This produces a registration.
:bulb: In the end, a registration is a type attached to function. This function is responsible to get an instance of the specified type based on the provided factory, value, alias or constructor.
Resolution Context
In dependency injection container terminology, a Handless resolution context corresponds to a scope. A scope is often referred as a kind of unique "sub container" for a short(er) duration of time. For example, in a HTTP API, you can have one scope per HTTP request. This allows to introduce a "scoped" lifetime to have the container create one instance of a type per scope (and then per request).
In order to resolve any types from a container, a handless.ResolutionContext must always be opened and used.
:warning: You're free to manage your contexts the way you want but using a single context for the whole application duration could be a code smell.
You can not resolve types from the container directly. This design choice has been made for two reasons:
- Avoid keeping transient values for the whole duration of a container and as a consequence, an application.
:question: This is because there is no reliable and easy way in Python to automatically cleanup object before garbage collection. Explicit cleanup is required or at least strongly encouraged.
- Avoid to raise errors when trying to resolve a soped type from a container instead of a scope
:question: For types registered with a lifetime of a
ResolutionContext(i.e: a scope) the question is "what should we do when resolving this from a container? Should we raise an error? Should we resolve it and consider the container as a scope as well?" Our design completly get rid of this choice by forcing usage of a context (scope), always
Lifetimes
When registering your types you can specify a lifetime. The lifetime determines when the container will execute or get a cached value of the function attached to the type to resolve:
-
handless.Singleton- On first resolve, the type function is called and its return value is cached for the whole duration of the container and for contexts
- Singletons are cached in the container itself
- Singletons context managers (if any) are entered on first resolve and exited on container end (release)
-
handless.Contextual- The type function is called and cached once per context. Additional resolve on the same context always return the same cached value
- Contextuals are cached per resolution context
- Contextuals context managers (if any) are entered on first resolve and exited on context end (release)
-
handless.Transient- The type function is called on each resolve.
- Transient values are never cached
- Transient context managers (if any) are entered on resolve and exited on context end (release)
:warning: You must understand that whichever lifetime you choose the container does not actually check returned object identity. The lifetime only determines when the container should execute registered functions or return a previously cached value. In other words, it means that you could register a transient type with a function returning always the same constant. You'll then end up with a singleton anyway.
:bulb: To avoid any troubles or misunderstanding regarding lifetimes when registering factories, ensure that your factories always create new instance of your object and does not do any manual caching upfront. Let the container take care of caching.
Getting started
Install it through your preferred package manager:
pip install handless
Once installed, you can create and use a container. Here is an example.
import smtplib
from dataclasses import dataclass
from typing import Protocol
from handless import Container, Contextual, ResolutionContext, Singleton, Transient
@dataclass
class User:
email: str
@dataclass
class Config:
smtp_host: str
class UserRepository(Protocol):
def add(self, cat: User) -> None: ...
def get(self, email: str) -> User | None: ...
class InMemoryUserRepository(UserRepository):
def __init__(self) -> None:
self._users: list[User] = []
def add(self, user: User) -> None:
self._users.append(user)
def get(self, email: str) -> User | None:
for user in self._users:
if user.email == email:
return user
return None
class NotificationManager(Protocol):
def send(self, user: User, message: str) -> None: ...
class StdoutNotificationManager(NotificationManager):
def send(self, user: User, message: str) -> None:
print(f"{user.email} - {message}") # noqa: T201
class EmailNotificationManager(NotificationManager):
def __init__(self, smtp: smtplib.SMTP) -> None:
self.server = smtp
self.server.noop()
def send(self, user: User, message: str) -> None:
msg = f"Subject: My Service notification\n{message}"
self.server.sendmail(
from_addr="myservice@example.com", to_addrs=[user.email], msg=msg
)
class UserService:
def __init__(
self, users: UserRepository, notifications: NotificationManager
) -> None:
self.users = users
self.notifications = notifications
def create_user(self, email: str) -> None:
user = User(email)
self.users.add(user)
self.notifications.send(user, "Your account has been created")
def get_user(self, email: str) -> User:
user = self.users.get(email)
if not user:
msg = f"There is no user with email {email}"
raise ValueError(msg)
return user
config = Config(smtp_host="stdout")
container = Container()
container.register(Config).value(config)
# User repository
container.register(InMemoryUserRepository).self(lifetime=Singleton())
container.register(UserRepository).alias(InMemoryUserRepository) # type: ignore[type-abstract]
# Notification manager
container.register(smtplib.SMTP).factory(
lambda ctx: smtplib.SMTP(ctx.resolve(Config).smtp_host),
lifetime=Singleton(),
enter=True,
)
container.register(StdoutNotificationManager).self(lifetime=Transient())
container.register(EmailNotificationManager).self()
@container.factory
def create_notification_manager(
config: Config, ctx: ResolutionContext
) -> NotificationManager:
if config.smtp_host == "stdout":
return ctx.resolve(StdoutNotificationManager)
return ctx.resolve(EmailNotificationManager)
# Top level service
container.register(UserService).self(lifetime=Contextual())
with container.open_context() as ctx:
service = ctx.resolve(UserService)
service.create_user("hello.world@handless.io")
# hello.world@handless.io - Your account has been created
print(service.get_user("hello.world@handless.io")) # noqa: T201
# User(email='hello.world@handless.io') # noqa: ERA001
container.release()
Core
Create a container
To create a container simply create an instance of it. You can use your container in a context manager or manually call its release method to cleanup all objects resolved so far.
:bulb:
.release()does not prevent from reusing your container afterwards.
>>> from handless import Container
>>> container = Container()
>>> # Use your container and release objects on exit
>>> with container:
... pass
>>> # Manually release
>>> container.release()
There should be at most one container per entrypoint in your application (a CLI, a HTTP server, ...). You can share the same container for all your entrypoints. A test is considered as an entrypoint as well.
:bulb: The container should be placed on your application composition root. This can be as simple as a
bootstrap.pyfile on your package root.
:warning The container is the most "high level" component of your application. It can import anything from any sub modules. However, none of your code should depends on the container itself. Otherwise you're going to use the service locator anti-pattern. There can be exceptions to this rule, for example, when used in an HTTP API controllers (as suggested in
svcs).
Open a context
To resolve any type from your container you must open a context first. The context should be released when not necessary anymore.
:bulb: Opened context are automatically released on container release if the context still has a strong reference to it.
from handless import Container
container = Container()
# You can manually open and release your context
ctx = container.open_context()
ctx.resolve(...)
ctx.release()
# Or do it with a context manager
with container.open_context():
ctx.resolve(...)
Context are of type handless.ResolutionContext.
:bulb: We did not chose
handless.Contextto avoid confusion with other contexts objects from other libraries.
Register a value
You can register a value directly for your type. When resolved, the provided value will be returned as-is.
from handless import Container
class Foo:
pass
foo = Foo()
container = Container()
container.register(Foo).value(foo)
resolved_foo = container.open_context().resolve(Foo)
assert resolved_foo is foo
Register a factory function
If you're looking for lazy instantiating your objects you can instead register a factory. A factory is a callable taking no or several arguments and returning an instance of the type registered. The callable can be a function, a method or even a type (a class). During resolution, the container will take care of calling the factory and return its return value. If your factory takes arguments, the container will first resolve its arguments using their type annotations and pass them to the factory.
:warning: your callable arguments must have type annotation to be properly resolved. If missing, an error will be raised at registration time.
:bulb: You do not need to create a dedicated factory function. There is nothing that prevents you from using an already existing function from standard library or any other library as long as it has typed parameters (or no parameters).
from handless import Container
class Foo:
def __init__(self, bar: int) -> None:
self.bar = bar
def create_foo(bar: int) -> Foo:
return Foo(bar)
container = Container()
container.register(int).value(42)
container.register(Foo).factory(create_foo)
resolved_foo = container.open_context().resolve(Foo)
assert isinstance(resolved_foo, Foo)
assert resolved_foo.bar == 42
Using factory decorator
Having to write your factory function somewhere then register it on your container elsewhere tends to reduce readability. If you prefer you can opt for using the factory decorator instead.
from handless import Container
class Foo:
def __init__(self, bar: int) -> None:
self.bar = bar
container = Container()
container.register(int).value(42)
@container.factory
def create_foo(bar: int) -> Foo:
return Foo(bar)
resolved_foo = container.open_context().resolve(Foo)
assert isinstance(resolved_foo, Foo)
assert resolved_foo.bar == 42
This is mostly a matter of preference as both ways do the exact same thing. You can also pass parameters to the factory decorator @factory(lifetime=..., enter=...).
Register a lambda function
When registering a factory, you can also pass a lambda function. However, as lambdas arguments can not have type annotation it is handled differently. Lambdas can take 0 or 1 argument. If one is given, a ResolutionContext object will be passed, when called at resolution, as the only argument. This allows you to resolve nested types if required.
from handless import Container
class Foo:
def __init__(self, bar: int) -> None:
self.bar = bar
container = Container()
container.register(int).value(42)
container.register(Foo).factory(lambda ctx: Foo(ctx.resolve(int)))
resolved_foo = container.open_context().resolve(Foo)
assert isinstance(resolved_foo, Foo)
assert resolved_foo.bar == 42
Register a type constructor
When you want to register a type and use its constructor (__init__ method) as its own factory, you can use the self() method instead of using .factory(MyType).
from handless import Container
class Foo:
def __init__(self, bar: int) -> None:
self.bar = bar
container = Container()
container.register(int).value(42)
container.register(Foo).self() # Same as: container.register(Foo).factory(Foo)
resolved_foo = container.open_context().resolve(Foo)
assert isinstance(resolved_foo, Foo)
assert resolved_foo.bar == 42
Register an alias
When you want a type to be resolved using resolution of another type you can define an alias.
:bulb: Useful for registering concrete implementations to protocols or abstract classes
from typing import Protocol
from handless import Container
class IFoo(Protocol):
pass
class Foo(IFoo):
def __init__(self) -> None:
pass
foo = Foo()
container = Container()
container.register(Foo).value(foo)
container.register(IFoo).alias(Foo)
resolved_foo = container.open_context().resolve(IFoo)
assert resolved_foo is foo
When resolving IFoo, the container will actually resolve and returns Foo.
Manage lifetime
During registration of factories .factory(...), @container.factory() and .self() you can optionally pass a lifetime.
:warning: You can not change lifetimes for
.value(...)and.alias(...)by design.
Lifetimes are actual objects and not enum constants nor literals.
from handless import Container, Singleton, Transient, Contextual
container = Container()
# Singleton
container.register(object).factory(lambda: object(), lifetime=Singleton())
# Contextual
container.register(object).factory(lambda: object(), lifetime=Contextual())
# Transient (The default)
container.register(object).factory(lambda: object(), lifetime=Transient())
As described above, lifetimes allow to determine when the container will execute types factory and cache their result. Generally speaking you may use:
handless.Singletonfor any objects that should be a singleton for your whole application (one and only one instance per application). For example a HTTP connection pool:warning: Singleton should be threadsafe in multi threaded application to avoid any issues
handless.Contextualfor objects that should be unique per context. For example, a database session should be unique per HTTP requesthandless.Transient(the default) for stateful objects which should not be shared because their use rely on their internal state. For example an opened file
Context managers and cleanup
Containers and contexts can take care of entering and exiting objects with context managers. Both has a release function which clear their cache and exits any entered context managers.
Factories
Object returned by functions registered with .factory(...) or .self() are automatically entered on resolve and exited on release if it is context managers.
:bulb: You can disable this default behavior by passing
enter=False. However, passingFalseis disallowed if the object return is NOT an instance of the given type.
:warning: Objects are only entered when resolved. Cached values are NOT re-entered afterwards.
If you pass a function which is a generator it will be automatically wrapped as a context manager (contextlib.contextmanager).
:bulb: You pass a function already decorated with
contextlib.contextmanagerand it will work as expected.
Values
Objects registered with .value(...) are NOT entered by default. If you want their context manager to be handled for you you must pass .value(..., enter=True).
:question: Passing a value means that this value has been created outside of the container and then its lifetime should not container's responsibility.
Context local registry
:construction: Under construction
Override container registrations
Containers does not allow to register the same type twice. The following code will raise an error.
from handless import Container
container = Container()
container.register(str).value("Hello")
container.register(str).value("This will raise an error!")
In order to override your container registered types you must use the override(...) function instead. This function works identically to register(...).
from handless import Container
container = Container()
container.register(str).value("Hello")
def test_my_container():
container.override(str).value("Overriden!")
with container.open_context() as ctx:
resolved = ctx.resolve(str)
assert resolved == "Overriden!"
:warning: Overriding is primarily made for testing purposes. You should not use overriding in your production code. If you have use cases where it could makes sense please open a ticket.
Please also note the following:
- Overrides can be overriden as well (each override erase the previous one)
- Overrides always take precedence over registered type whatever his lifetime (even if the type was previously resolved and cached)
- Overrides are automatically erased when the container is released
- On container release, all overrides (even erased one) as well as any previously registered types are properly released as well
Recipes
Release container on application exits
If your application has no shutdown mechanism you can register your container release method using atexit module to release on program exit.
import atexit
from handless import Container
container = Container()
atexit.register(container.release)
Releasing the container is idempotent and can be used several times. Each time, all singletons will be cleared and then context manager exited, if any.
Register primitive types
:construction: Under construction
Register same type for different purposes
:construction: Under construction
Register implementations for protocols and abstract classes
Dependency injection is a key enabler for inversion of control where your objects depends on abstractions or interfaces rather than actual implementation. This mechanism prevents tight coupling between your objects and allows you to swap dependencies with different implementations. This mechanism is mostly used for testing purposes to replace real implementations with fakes or mocks.
handless allows you to do so through various mechanisms. Let's consider you defined an interface of a repository with two implementations, one fo mongoDB and another for SQLite.
:warning: Unrelevant details have been removed for readability.
from typing import Protocol
from handless import Container
class TodoItemRepository(Protocol):
def add(self, todo: dict) -> None: ...
class MongoTodoItemRepository(TodoItemRepository):
def __init__(self, mongo_url: str) -> None: ...
def add(self, todo: dict) -> None: ...
class SqliteTodoItemRepository(TodoItemRepository):
def __init__(self) -> None: ...
def add(self, todo: dict) -> None: ...
container = Container()
Static registration
The most simple case is when you want to statically define which implementation use. Then you'll eventually override this during your tests.
# Individually register your implementations
container.register(SqliteTodoItemRepository).self()
container.register(MongoTodoItemRepository).factory(
lambda ctx: MongoTodoItemRepository(os.getenv("DB_URL"))
)
# Register an alias of your protocol against your choice
container.register(TodoItemRepository).alias(SqliteTodoItemRepository) # type: ignore[type-abstract]
with container.open_context() as ctx:
repo = ctx.resolve(TodoItemRepository) # type: ignore[type-abstract]
assert isinstance(repo, SqliteTodoItemRepository)
:warning: Mypy does not like calling the
.registerand.resolvefunctions ontyîng.Protocolnorabc.ABChence the type ignore magic comment.
Runtime registration
There can also be situations where you want to pick implementation at runtime depending on some conditions. For this, you can use a factory that will resolve the correct implementation.
import os
from handless import Singleton, Contextual, ResolutionContext
container.register(SqliteTodoItemRepository).self()
container.register(MongoTodoItemRepository).factory(
lambda ctx: MongoTodoItemRepository(os.getenv("DB_URL"))
)
@container.factory
def get_todo_item_repository(ctx: ResolutionContext) -> TodoItemRepository:
db_type = os.getenv("DB_TYPE", "sqlite")
if db_type == "sqlite":
return ctx.resolve(SqliteTodoItemRepository)
if db_type == "mongo":
return ctx.resolve(MongoTodoItemRepository)
raise ValueError(f"Unknown database type: {db_type}")
:bulb: Use of the factory decorator is not mandatory. You can achieve the same with the registration API (
container.register(...)).
:warning: Most of the time you should use a
Transientlifetime (the default) for the factory resolving your abstract or protocol to avoid lifetimes mismatches. Indeed, if you use aSingletonlifetime onget_todo_item_repositorywhile one of your implementation isTransientorContextualyou'll end up with a captive dependency.
Testing
:construction: Under construction
Use with FastAPI
:construction: Under construction
Use with Typer
:construction: Under construction
Add custom lifetime(s)
:construction: Under construction
Q&A
Why requiring having a context object to resolve types instead of using the container directly?
- Separation of concerns
- Simpler API
- Transient dependencies captivity
- Everything is a context
- Easier management and release of resolved values
Why using a fluent API to register types as a two step process?
- type hints limitations
Why using objects for lifetimes? (Why not using enums or literals?)
- Allow creating its own lifetimes
- Allows to add options in the future
- Avoid if statements
Alternatives
Other existing alternatives you might be interested in:
Contributing
Running tests: uv run nox
:warning: As this library support both sync and async functions, tests have been duplicated for simplicity. Whenever you add, remove or change an existing test in
test_resolve.pyortest_resolve_async.pydon't forget to update each others.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file handless-0.3.0.tar.gz.
File metadata
- Download URL: handless-0.3.0.tar.gz
- Upload date:
- Size: 894.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.8.22
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
bb443292ea3930ae1996f7a89f1eb9ff4d92a770a693e5d8083f7eb4e932d605
|
|
| MD5 |
bc5471d68505b47a04e96effefc5fd93
|
|
| BLAKE2b-256 |
12e222985e8c40142fc76ee7e6fe0e2f5b2c829ead38134edcc401cac9c4e049
|
File details
Details for the file handless-0.3.0-py3-none-any.whl.
File metadata
- Download URL: handless-0.3.0-py3-none-any.whl
- Upload date:
- Size: 22.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.8.22
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d94765af66dec70d327955b4f70e57e40ce52a0491f7d0985f8bd4ca5ccd2435
|
|
| MD5 |
ecca9c18e6a7ba87ec5023665517b663
|
|
| BLAKE2b-256 |
d761e8db73d1f2660c4ac4573c38ba369f8d72397d2a88609afebd6e80984002
|