hazrakah (הזרקה) is a tiny but powerful DI library for Python.
This README is only a high-level introduction to hazrakah. For more detailed documentation, please view the official docs at https://hazrakah.readthedocs.io.
- Supports Singleton, Transient, and Instance lifetimes.
- Hierarchical scoping; Isolate registrations and/or resolves. optionally use a context manager to deterministically tear down a scope and its resolved objects.
- Namespaced Registrations; Register types into named scopes and resolve with priority chains. Allows multiple implementations of the same interface to coexist.
- Protocols, ABCs, and Concretes can be registered against Factory Functions and Concretes.
- Lifetime Decorators; (OPTIONAL) Types decorated with
@singleton,@transientor@instancedcan be registered with a single call toregister_decorated(), simplifying orchestration. - Implicit Multi-Registration; Types decorated with
@providesbind to all provided types (unless explicit types are specified during registration.) - Fluent API; All registration methods return
self, enabling method-chained container setup.
You can install hazrakah from PyPI through usual means, such as pip:
pip install hazrakahhazrakah manages object lifecycles through three registration strategies:
from hazrakah import Container
container = Container()
# TRANSIENT — a new instance for every resolve.
container.register_transient(IFoo, Foo)
assert container.resolve(IFoo) is not container.resolve(IFoo)
# SINGLETON — one shared instance across all resolves in scope.
container.register_singleton(IFooBar, lambda c: c.resolve(FooBarImpl))
assert container.resolve(IFooBar) is container.resolve(IFooBar)
# INSTANCE — your exact object, returned everywhere (including child scopes).
bar_obj = Bar()
container.register_instance(IBar, bar_obj)
assert container.resolve(IBar) is bar_objScopes provide isolation: parent registrations flow down, but child-only registrations stay local.
parent = Container()
child = parent.create_scope()
parent.register_transient(IFoo, Foo)
child.resolve(IFoo) # resolves parent's registration
child.register_transient(IBar, Bar)
child.resolve(IBar) # works — registered in this scope
# parent.resolve(IBar) # raises KeyError — invisible to parentResolve tracked resources and get deterministic teardown when the scope exits.
from hazrakah import Container
class Closeable:
def __init__(self): self.closed = False
def close(self): self.closed = True
with Container() as c:
c.register_transient(Closeable)
res = c.resolve(Closeable)
assert res.closed # teardown ran automatically on __exit__All registration methods return self, enabling method-chained setup.
container = (
Container()
.register_transient(IFoo, Foo)
.register_singleton(IBar, Bar)
.register_instance(IFizz, Fizz())
)
assert isinstance(container.resolve(IFoo), Foo)
assert isinstance(container.resolve(IBar), Bar)The Cached[T] class wraps a factory callable so its result is produced once and re-used until the TTL window elapses. The factory receives a resolver argument, matching hazrakah's standard factory contract (see :py:data:hazrakah.DependencyRegistry.Factory). Register it with any container lifetime to combine DI resolution with time-bound caching:
from datetime import timedelta
from hazrakah import Container, Cached
class ConfigSource:
def load(self) -> str:
return 'loaded'
# TTL accepts float (seconds) or timedelta; default is 47.0 seconds.
cache = Cached(lambda c: ConfigSource(), ttl=timedelta(seconds=47))
first = cache(object()) # factory called once (TTL not yet elapsed)
second = cache(object()) # cached value returned; factory not re-invoked
assert first is second # same instanceEvery Cached instance exposes a ttl read-only property and a reset() method for manual cache eviction:
cache = Cached(lambda c: ConfigSource(), ttl=timedelta(seconds=0))
# Zero TTL — factory called on every access.
assert cache(object()) is not cache(object()) # two distinct instances
cache.reset() # discard cached value
_ = cache(object()) # re-invokes factoryYou can also pass ttl as a plain float (seconds):
cache = Cached(lambda c: ConfigSource(), ttl=120.0) # 120 seconds
assert cache.ttl == timedelta(seconds=120)Mark intent at class-definition time with @singleton, @transient, or @instanced, then register everything in one call.
from hazrakah import Container, singleton, transient, instanced
@singleton(types=IFoo)
class FooService: ...
@transient(types=IBar)
class BarService: ...
@instanced # binds to the class itself
class BuzzService: ...
c = Container()
c.register_decorated() # discovers all decorated classes
assert c.resolve(IFoo) is c.resolve(IFoo) # singleton
assert c.resolve(IBar) is not c.resolve(IBar) # transientDeclare which interfaces a class implements; registration binds to all of them simultaneously.
from hazrakah import Container, provides
@provides(IFoo, IBar)
class MultiImpl:
def foo(self): ...
def bar(self): ...
c = Container()
c.register_transient(MultiImpl) # registers under IFoo, IBar, and MultiImpl
a = c.resolve(IFoo)
b = c.resolve(IBar)
assert a is b # same cached singleton instanceThe @provides decorator is a passive marker -- it stores metadata only, with zero registration logic at decoration time. Activation depends entirely on how the container later registers the decorated class.
@provides activates when you call register_singleton, register_transient, or register_instance with no second argument (no explicit type override):
@provides(IFoo, IBar)
class MultiImpl: ...
c.register_singleton(MultiImpl) # multi-registers under IFoo + IBar + MultiImpl
c.resolve(IFoo) # works -- @provides activated
c.resolve(IBar) # works -- @provides activated@provides does NOT activate when you provide an explicit type argument to a registration method:
@provides(IBar)
class MultiImpl: ...
c.register_singleton(IFoo, MultiImpl) # only IFoo is registered
c.resolve(IFoo) # works -- explicit registration
c.resolve(IBar) # raises KeyError -- @provides was ignoredThis is intentional. The second positional argument on any register_* method is the explicit type override. When you provide it, you are telling the container exactly which key to register against -- and @provides does not interfere.
| Registration call | @provides activates? |
Registered keys |
|---|---|---|
register_singleton(MyClass) |
YES | MyClass + all @provides interfaces |
register_singleton(IFoo, MyClass) |
NO | Only IFoo |
register_transient(MyClass) |
YES | MyClass + all @provides interfaces |
register_transient(IFoo, MyClass) |
NO | Only IFoo |
register_instance(my_obj) (no explicit instance) |
YES | type(obj) + all @provides interfaces |
register_instance(IFoo, my_obj) (explicit instance) |
NO | Only IFoo |
You can reach me on Discord or open an Issue on Github.