Lightweight dependency injection container with auto-wiring and scoped child containers.
Project description
miniject
Lightweight dependency injection container for Python. Auto-wires constructor dependencies from type hints, supports singleton and transient scopes, and provides scoped child containers for testing and per-context overrides.
Small codebase. Zero dependencies. Fully typed.
Installation
pip install miniject
Quick start
from miniject import Container
class Database:
def __init__(self, url: str = "sqlite:///:memory:") -> None:
self.url = url
class UserRepo:
def __init__(self, database: Database) -> None:
self.database = database
class UserService:
def __init__(self, repo: UserRepo) -> None:
self.repo = repo
container = Container()
container.bind(Database, instance=Database("postgres://localhost/mydb"))
container.bind(UserRepo) # auto-wired from type hints
container.bind(UserService) # resolves UserRepo → Database automatically
service = container.resolve(UserService)
assert service.repo.database.url == "postgres://localhost/mydb"
API
Container()
Create a new container.
container.bind(service, *, factory=..., instance=..., singleton=...)
Register a service type. Four modes:
| Call | Behavior |
|---|---|
bind(SomeType) |
Auto-wire from __init__ type hints (transient) |
bind(SomeType, instance=obj) |
Singleton by instance |
bind(SomeType, factory=fn) |
Custom factory (transient) |
bind(SomeType, factory=fn, singleton=True) |
Custom factory (singleton, shared by child scopes) |
Auto-wiring inspects constructor parameters via typing.get_type_hints()
and resolves each typed parameter from the container. Parameters with default
values are left to Python when no binding exists. Nullable dependencies such as
Database | None = None are supported: if Database is bound it is injected,
otherwise Python keeps the default None.
Common scalar builtins like int, str, float, bool, and bytes are not
supported as DI keys. Prefer typed value objects or explicit factories for
scalar configuration values.
Type hints must be importable at runtime. If get_type_hints() cannot resolve
an annotation, miniject raises ResolutionError instead of silently skipping
injection. Annotated[...] is intentionally unsupported. miniject does not
provide qualifier-style multiple bindings for the same base type. If two
dependencies mean different things, model them as different types. If the
distinction is construction logic, use an explicit factory.
Design Philosophy
miniject prefers minimal container magic:
- constructors should describe semantic dependencies, not container selection rules
- composition roots and factories should own non-trivial wiring decisions
- if two dependencies mean different things, they should usually be different types
- if construction depends on runtime policy, use an explicit factory rather than metadata
container.resolve(service, **overrides)
Resolve a service, recursively auto-wiring all dependencies. Keyword
overrides are passed directly to the factory/constructor, bypassing the
container for those parameters.
Raises ResolutionError on missing bindings or circular dependencies, with
a full dependency chain in the message.
container.scope()
Create a child container that inherits all parent bindings. Overrides in the child do not affect the parent.
parent = Container()
parent.bind(Database, instance=production_db)
parent.bind(UserRepo)
child = parent.scope()
child.bind(Database, instance=test_db) # override in child only
child.resolve(UserRepo).database # → test_db
parent.resolve(UserRepo).database # → production_db
Singletons defined in the parent remain shared when resolved through a child scope. If a child re-binds a service, that override is isolated to the child.
Use cases:
- Testing — swap specific dependencies without rebuilding the whole graph
- Per-request isolation — override config for a specific context
ResolutionError
Raised when resolution fails. The message includes the full dependency chain:
ResolutionError: Cannot resolve UserRepo: missing binding for parameter 'database'
(type=Database) (UserService -> UserRepo)
Circular dependencies are detected and reported:
ResolutionError: Circular dependency: A -> B -> A
Composition root pattern
Only composition roots (startup code, CLI entrypoints, test fixtures) should
call container.resolve(). All other code receives dependencies via constructor
injection:
# src/myapp/container.py — composition root
from miniject import Container
def create_container(config: Config) -> Container:
c = Container()
c.bind(Config, instance=config)
c.bind(Database, factory=lambda: Database(config.db_url), singleton=True)
c.bind(UserRepo)
c.bind(UserService)
return c
# src/myapp/services.py — normal code, no container import
class UserService:
def __init__(self, repo: UserRepo) -> None:
self.repo = repo
Thread safety
miniject is designed for the composition-root-at-startup pattern: build
and populate the container at application start, then share it for resolution.
Concurrent resolve() calls are safe after configuration is complete, and
singleton factories are initialized at most once per owning container.
Rebinding services on a container that is already being shared across threads is not supported. If you need runtime reconfiguration, build a new container or a child scope instead of mutating a shared container in place.
When to use miniject
miniject is a good fit when you want:
- constructor injection from type hints
- a tiny composition-root container with very little magic
- child scopes for tests and context-specific overrides
- explicit failure when runtime annotations are not actually resolvable
miniject is probably not the right fit when you need:
- async/resource lifecycle management
- framework integration or function/method wiring
- multiple qualified bindings for the same base type
- extensive provider types, configuration loaders, or container metaprogramming
Comparison
miniject is intentionally narrower than larger Python DI frameworks.
- Compared with
dependency-injector, miniject is much smaller and easier to hold in your head, but it does not try to compete with provider graphs, configuration providers, wiring, async resources, or broader framework integrations. - Compared with
lagom, miniject is more opinionated and lower-surface-area. Lagom supports async usage, richer integrations, and more advanced type-driven behavior. miniject aims to stay focused on composition-root constructor injection. - Compared with
punq, miniject lives in a more similar simplicity tier. The main differences are miniject's scoped child containers, circular dependency detection, and stricter stance on runtime-resolvable type hints.
The goal is not to be the most powerful DI library. The goal is to be a small, predictable one that stays useful without turning into a framework.
License
MIT
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 miniject-0.1.1.tar.gz.
File metadata
- Download URL: miniject-0.1.1.tar.gz
- Upload date:
- Size: 10.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.10
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b50019858f45986c5a03979f021528e4b46c2b92a94f3261268868870b7dd5e2
|
|
| MD5 |
4c414dcd7d0af6a7ecd1f3fca7a51925
|
|
| BLAKE2b-256 |
e61839e37f06ad23fc70bdb02ae4f76d36d0afe3b4924a416813c81cb28e3cb8
|
File details
Details for the file miniject-0.1.1-py3-none-any.whl.
File metadata
- Download URL: miniject-0.1.1-py3-none-any.whl
- Upload date:
- Size: 8.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.10
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8e36313084c504dfb35687fb12b76ce6b019fa810d47c963ed7e0e5dbf4e245d
|
|
| MD5 |
f72a26d8ff99b44ef1d3e42353a22d37
|
|
| BLAKE2b-256 |
ae01a6758d3f1164f40ae2aeb7b937da10bc8878f7eab6a230014a5ad6e89d4c
|