A powerful and type-safe dependency injection/IoC library for Python
Project description
inversipy
A powerful and type-safe dependency injection/IoC (Inversion of Control) library for Python.
Features
- Type annotation-based dependency resolution - Dependencies are resolved using Python type hints
- Container validation - Ensure all dependencies can be resolved before runtime
- Module system - Organize dependencies with public/private access control
- Parent-child container hierarchy - Create child containers that inherit from parent
- Multiple scopes - Singleton, Transient, and Request scopes
- Function injection - Run functions with automatic dependency injection via
container.run() - Property injection - Injectable base class for clean, declarative dependency injection
- Named dependencies - Register multiple implementations with names for disambiguation
- Collection injection - Register multiple implementations and inject as a collection with
InjectAll - Named collections - Group implementations by name and inject with
InjectAllNamed - Async support - First-class support for async dependencies
- Type-safe - Full type hint support for better IDE integration
- Pure classes - No container coupling - classes remain framework-agnostic
Installation
pip install inversipy
For development:
pip install inversipy[dev]
Quick Start
from inversipy import Container, Scopes
# Define your services
class Database:
def query(self, sql: str) -> list:
return ["result"]
class UserRepository:
def __init__(self, db: Database) -> None:
self.db = db
def get_users(self) -> list:
return self.db.query("SELECT * FROM users")
class UserService:
def __init__(self, repo: UserRepository) -> None:
self.repo = repo
def list_users(self) -> list:
return self.repo.get_users()
# Create container and register dependencies
container = Container()
container.register(Database, scope=SINGLETON)
container.register(UserRepository)
container.register(UserService)
# Validate container (optional but recommended)
container.validate()
# Resolve dependencies
service = container.get(UserService)
users = service.list_users()
Core Concepts
Architecture Overview
Inversipy's architecture is built on two core abstractions:
- Container: The base class that provides dependency registration, resolution, and composition. Supports parent-child hierarchies and module registration. All dependencies are public by default.
- Module: Extends Container to add public/private access control. Modules can selectively expose dependencies and register other modules for composition.
This design eliminates code duplication while providing proper specialization - use Container for simplicity, and Module when you need encapsulation.
Container
The Container is the main component that manages dependency registration and resolution.
from inversipy import Container, Scopes, TRANSIENT
container = Container()
# Register with automatic resolution
container.register(MyService)
# Register with explicit implementation
container.register(IService, implementation=MyServiceImpl)
# Register with factory function
container.register_factory(MyService, lambda: MyService("config"))
# Register with pre-created instance
instance = MyService()
container.register_instance(MyService, instance)
# Resolve dependencies
service = container.get(MyService)
# Check if registered
if container.has(MyService):
service = container.get(MyService)
# Try to get (returns None if not found)
service = container.try_get(MyService)
Scopes
Scopes control the lifecycle of dependencies.
Singleton Scope
Creates one instance and reuses it for all requests:
from inversipy import Container, Scopes
container = Container()
container.register(Database, scope=SINGLETON)
db1 = container.get(Database)
db2 = container.get(Database)
assert db1 is db2 # Same instance
Transient Scope
Creates a new instance for each request:
from inversipy import Container, Scopes
container = Container()
container.register(RequestHandler, scope=TRANSIENT)
handler1 = container.get(RequestHandler)
handler2 = container.get(RequestHandler)
assert handler1 is not handler2 # Different instances
Request Scope
Creates one instance per request/context using Python's contextvars module. Automatically isolates instances per async task or thread - no manual context management needed:
from inversipy import Container, Scopes
container = Container()
container.register(RequestService, scope=REQUEST)
# Automatic isolation - each async task gets its own instance
async def handle_request():
service = container.get(RequestService)
# Each concurrent request automatically gets isolated instances
# The framework's context (async task, thread) is automatically used
return service.process()
# Within the same context, you get the same instance
def sync_handler():
service1 = container.get(RequestService)
service2 = container.get(RequestService)
assert service1 is service2 # Same instance in same context
Modules
Modules allow you to organize dependencies with public/private access control. Dependencies are private by default - you must explicitly mark them as public. Modules are registered as live providers - they remain the source of truth for their dependencies.
from inversipy import Module, Container, Scopes
# Create a database module
db_module = Module("Database")
# Register private dependencies (public=False is the default)
db_module.register(DatabaseConnection, scope=Scopes.SINGLETON) # Private by default
db_module.register(QueryBuilder) # Private by default
# Register public dependencies (must explicitly set public=True)
db_module.register(Database, scope=Scopes.SINGLETON, public=True)
db_module.register(UserRepository, public=True)
# Or use export to make dependencies public after registration
db_module.export(Database, UserRepository)
# Register module as a provider in the container
container = Container()
container.register_module(db_module)
# Only public dependencies are accessible
database = container.get(Database) # ✓ Works - public
user_repo = container.get(UserRepository) # ✓ Works - public
# connection = container.get(DatabaseConnection) # ✗ DependencyNotFoundError - private
# Module remains live - add new dependencies dynamically
db_module.register(CacheService, public=True)
cache = container.get(CacheService) # ✓ Works! Module is still connected
Modules can also register other modules for composition:
# Create specialized modules
auth_module = Module("Auth")
auth_module.register(AuthService, public=True)
auth_module.register(TokenValidator, public=False)
db_module = Module("Database")
db_module.register(Database, public=True)
# Create an app module that composes other modules
app_module = Module("App")
app_module.register_module(auth_module) # Import auth module
app_module.register_module(db_module) # Import db module
app_module.register(AppService, public=True)
# App module can access public dependencies from registered modules
container = Container()
container.register_module(app_module)
# All public dependencies are accessible
auth = container.get(AuthService) # From auth_module
db = container.get(Database) # From db_module
app = container.get(AppService) # From app_module
Using ModuleBuilder:
from inversipy import ModuleBuilder, SINGLETON
module = (
ModuleBuilder("Database")
.bind(DatabaseConnection, scope=SINGLETON) # Private
.bind(QueryBuilder) # Private
.bind_public(Database, scope=SINGLETON) # Public
.bind_public(UserRepository) # Public
.build()
)
Parent-Child Containers
Create container hierarchies where children can access parent dependencies:
from inversipy import Container, Scopes
# Parent container with shared services
parent = Container(name="Parent")
parent.register(Database, scope=SINGLETON)
parent.register(Config, scope=SINGLETON)
# Child container for a specific context
child = parent.create_child(name="RequestContainer")
child.register(RequestContext)
child.register(RequestHandler)
# Child can access parent dependencies
db = child.get(Database) # Resolved from parent
handler = child.get(RequestHandler) # Resolved from child
# Parent is not affected by child registrations
assert not parent.has(RequestHandler)
Validation
Validate that all dependencies can be resolved:
from inversipy import Container, ValidationError
container = Container()
container.register(ServiceA)
container.register(ServiceB) # Depends on ServiceA
container.register(ServiceC) # Depends on ServiceX (not registered)
try:
container.validate()
except ValidationError as e:
print(f"Validation failed with {len(e.errors)} errors:")
for error in e.errors:
print(f" - {error}")
Function Injection with Container.run()
Run functions with automatic dependency injection using container.run():
from inversipy import Container, Scopes
container = Container()
# Pure classes - no decorator coupling
class Database:
def query(self, sql: str) -> list:
return []
class RequestHandler:
def __init__(self, db: Database) -> None:
self.db = db
# Register with pure registration
container.register(Database, scope=Scopes.SINGLETON)
container.register(RequestHandler)
# Pure function - no decorators
def handle_request(handler: RequestHandler) -> dict:
return {"status": "ok"}
# Use container.run() to inject dependencies
result = container.run(handle_request) # Dependencies automatically resolved
# Can also provide some arguments explicitly
result = container.run(handle_request, custom_arg="value")
Property Injection with Injectable
Property injection using Injectable base class:
from typing import Annotated
from inversipy import Container, Injectable, Inject
container = Container()
container.register(Database)
container.register(Logger)
class UserService(Injectable):
database: Annotated[Database, Inject]
logger: Annotated[Logger, Inject]
def get_users(self) -> list:
self.logger.info("Fetching users")
return self.database.query("SELECT * FROM users")
container.register(UserService)
service = container.get(UserService) # Dependencies auto-injected!
users = service.get_users()
The Injectable base class automatically:
- Scans for
Annotated[Type, Inject]properties - Generates a constructor that accepts these dependencies as parameters
- Keeps classes pure - they can be instantiated manually or via container
Classes using Injectable remain container-agnostic and can be used standalone:
# Manual instantiation - class is pure
my_db = Database()
my_logger = Logger()
service = UserService(database=my_db, logger=my_logger)
Named Dependencies
Register multiple implementations of the same interface using named dependencies:
from inversipy import Container, Inject, Named
class IDatabase:
pass
class PostgresDB(IDatabase):
pass
class SQLiteDB(IDatabase):
pass
container = Container()
container.register(IDatabase, PostgresDB, name="primary")
container.register(IDatabase, SQLiteDB, name="backup")
# Resolve by name
primary_db = container.get(IDatabase, name="primary")
backup_db = container.get(IDatabase, name="backup")
With property injection:
from inversipy import Injectable, Inject, Named
class DataService(Injectable):
primary_db: Inject[IDatabase, Named("primary")]
backup_db: Inject[IDatabase, Named("backup")]
Collection Injection
Register multiple implementations and inject them as a collection using InjectAll:
from inversipy import Container, InjectAll, Injectable
class IPlugin:
def execute(self) -> str:
raise NotImplementedError
class PluginA(IPlugin):
def execute(self) -> str:
return "PluginA executed"
class PluginB(IPlugin):
def execute(self) -> str:
return "PluginB executed"
# Multiple registrations accumulate (no overwriting)
container = Container()
container.register(IPlugin, PluginA)
container.register(IPlugin, PluginB)
# Get all implementations
plugins = container.get_all(IPlugin) # [PluginA(), PluginB()]
for plugin in plugins:
print(plugin.execute())
# Single resolution fails when ambiguous
# container.get(IPlugin) # raises AmbiguousDependencyError
With property injection:
class PluginManager(Injectable):
plugins: InjectAll[IPlugin]
def run_all(self) -> list[str]:
return [plugin.execute() for plugin in self.plugins]
container.register(PluginManager)
manager = container.get(PluginManager)
results = manager.run_all() # ['PluginA executed', 'PluginB executed']
Migration Note: Multiple
register()calls for the same interface now accumulate rather than overwrite. Code that relied on overwriting behavior should either:
- Use named bindings:
container.register(IPlugin, NewImpl, name="main")- Or explicitly clear bindings before re-registering
Named Collection Injection
Combine named dependencies with collection injection using InjectAll[T, Named("x")]:
from inversipy import Container, InjectAll, Named, Injectable
# Register plugins in named groups
container = Container()
container.register(IPlugin, PluginA, name="core")
container.register(IPlugin, PluginB, name="core")
container.register(IPlugin, PluginC, name="optional")
# Get all implementations in a named group
core_plugins = container.get_all(IPlugin, name="core") # [PluginA(), PluginB()]
optional_plugins = container.get_all(IPlugin, name="optional") # [PluginC()]
With property injection:
class PluginManager(Injectable):
core_plugins: InjectAll[IPlugin, Named("core")]
optional_plugins: InjectAll[IPlugin, Named("optional")]
def run_core(self) -> list[str]:
return [p.execute() for p in self.core_plugins]
container.register(PluginManager)
manager = container.get(PluginManager)
manager.run_core() # Only runs core plugins
Advanced Usage
Factory Functions with Dependencies
Factory functions can have dependencies that are automatically resolved from the container. Simply type-hint the parameters, and the container will inject them:
from inversipy import Container, Scopes
container = Container()
container.register(Config, scope=Scopes.SINGLETON)
def create_database(config: Config) -> Database:
"""Factory function with dependency - config is automatically injected!"""
return Database(config.db_url)
# The container automatically resolves the Config dependency
container.register_factory(Database, create_database, scope=Scopes.SINGLETON)
# Config is injected automatically when creating Database
db = container.get(Database)
Works with multiple dependencies too:
def create_user_service(db: Database, logger: Logger, cache: Cache) -> UserService:
"""All three dependencies are automatically resolved and injected"""
return UserService(db, logger, cache)
container.register_factory(UserService, create_user_service)
service = container.get(UserService) # db, logger, and cache auto-injected
Conditional Registration
from inversipy import Container
container = Container()
if is_production:
container.register(ICache, implementation=RedisCache)
else:
container.register(ICache, implementation=InMemoryCache)
Multiple Containers
from inversipy import Container
# Application-wide container
app_container = Container(name="Application")
app_container.register(Database, scope=SINGLETON)
# Request-specific container
def handle_request(request):
request_container = app_container.create_child(name="Request")
request_container.register_instance(Request, request)
handler = request_container.get(RequestHandler)
return handler.handle()
RequestScope with Web Frameworks
RequestScope uses contextvars for automatic context isolation. No explicit context management needed - each request/thread/async task is automatically isolated:
from inversipy import Container, Scopes
from fastapi import FastAPI
app = FastAPI()
container = Container()
# Register request-scoped services
container.register(RequestLogger, scope=REQUEST)
container.register(DatabaseSession, scope=REQUEST)
@app.get("/api/users")
async def get_users():
# Each request automatically gets its own instances
# No context manager needed - FastAPI tasks are already isolated!
logger = container.get(RequestLogger)
db = container.get(DatabaseSession)
await logger.log("Fetching users")
users = await db.query("SELECT * FROM users")
return {"users": users}
@app.post("/api/users")
async def create_user(user_data: dict):
# Different concurrent requests get different instances automatically
logger = container.get(RequestLogger)
db = container.get(DatabaseSession)
await logger.log(f"Creating user: {user_data}")
# Each request has its own db session - thread-safe by default
await db.execute("INSERT INTO users ...", user_data)
return {"status": "created"}
Works with Flask (threading-based) too:
from flask import Flask
app = Flask(__name__)
@app.route('/api/users')
def get_users():
# Each thread (request) automatically gets isolated instances
logger = container.get(RequestLogger)
db = container.get(DatabaseSession)
logger.log("Processing request")
users = db.query("SELECT * FROM users")
return {"users": users}
The contextvars-based implementation provides:
- Zero configuration: Automatic isolation per request/task/thread
- Thread-safe: Each thread gets its own context automatically
- Async-aware: Works seamlessly with asyncio and concurrent requests
- Framework agnostic: Works with FastAPI, Flask, Starlette, etc.
- No manual management: The library leverages existing contexts created by your framework
Testing with Containers
import pytest
from inversipy import Container
@pytest.fixture
def container():
container = Container()
# Register test doubles
container.register(IDatabase, implementation=MockDatabase)
container.register(IEmailService, implementation=FakeEmailService)
return container
def test_user_service(container):
container.register(UserService)
service = container.get(UserService)
result = service.create_user("test@example.com")
assert result is not None
Best Practices
-
Validate early: Call
container.validate()at application startup to catch configuration errors early -
Use scopes appropriately:
SINGLETONfor expensive resources (database connections, caches)TRANSIENTfor stateful services (request handlers, commands)REQUESTfor request-scoped resources (in web applications)
-
Organize with modules: Group related dependencies into modules with clear public interfaces
-
Prefer constructor injection: Use type-annotated constructors for dependency injection
-
Use interfaces: Register interfaces and bind to implementations for better testability
-
Child containers for isolation: Use child containers for request-scoped or test-specific dependencies
-
Document public module interfaces: Clearly document which dependencies are public in your modules
Error Handling
Inversipy provides clear error messages:
from inversipy import (
DependencyNotFoundError,
CircularDependencyError,
ValidationError,
RegistrationError,
ResolutionError,
)
try:
service = container.get(UnregisteredService)
except DependencyNotFoundError as e:
print(f"Dependency not found: {e}")
try:
container.validate()
except ValidationError as e:
print(f"Validation failed: {len(e.errors)} errors")
for error in e.errors:
print(f" - {error}")
Type Safety
Inversipy is fully typed for better IDE support:
from inversipy import Container
container = Container()
container.register(Database)
# Type checkers understand the return type
db: Database = container.get(Database)
# IDE autocomplete works
db.query("SELECT * FROM users")
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
License
MIT License - see LICENSE file for details.
Similar Projects
If inversipy doesn't fit your needs, check out these alternatives:
Why "inversipy"?
The name combines "inversion" (as in Inversion of Control) with "py" (Python). It's about inverting control flow - instead of your code creating dependencies, the container provides them.
FastAPI Integration
Inversipy provides seamless FastAPI integration with the @inject decorator:
from typing import Annotated
from fastapi import FastAPI
from inversipy import Container
from inversipy.decorators import Inject
from inversipy.fastapi import inject
# Setup
app = FastAPI()
container = Container()
container.register(Database)
container.register(Logger)
app.state.container = container
# Use @inject to auto-resolve dependencies
@app.get("/users")
@inject
async def get_users(
db: Annotated[Database, Inject],
logger: Annotated[Logger, Inject],
limit: int = 10
):
logger.info(f"Fetching {limit} users")
return db.query("SELECT * FROM users LIMIT ?", limit)
The @inject decorator:
- Identifies parameters marked with
Annotated[Type, Inject] - Resolves them from the container automatically
- Leaves normal FastAPI parameters (query params, body, etc.) unchanged
- Works with both sync and async route handlers
Install FastAPI support:
pip install inversipy fastapi
Project details
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 inversipy-0.1.0.tar.gz.
File metadata
- Download URL: inversipy-0.1.0.tar.gz
- Upload date:
- Size: 29.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
22ad4bbb1b4cf7d34a48c9afc87d01be833f539583b98692e24c79512b3d0ab3
|
|
| MD5 |
1a92c0a4c2dc4e3a421c34441601f4e7
|
|
| BLAKE2b-256 |
a8bc05e28c81321ad7648f95e938170bdc32a16025110fe46516be197c301af0
|
Provenance
The following attestation bundles were made for inversipy-0.1.0.tar.gz:
Publisher:
publish.yml on mottetm/inversipy
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
inversipy-0.1.0.tar.gz -
Subject digest:
22ad4bbb1b4cf7d34a48c9afc87d01be833f539583b98692e24c79512b3d0ab3 - Sigstore transparency entry: 1019271176
- Sigstore integration time:
-
Permalink:
mottetm/inversipy@c24a613c8b1429193cd53540e50f5f8a43307513 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/mottetm
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@c24a613c8b1429193cd53540e50f5f8a43307513 -
Trigger Event:
push
-
Statement type:
File details
Details for the file inversipy-0.1.0-py3-none-any.whl.
File metadata
- Download URL: inversipy-0.1.0-py3-none-any.whl
- Upload date:
- Size: 28.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f42c2988bbaecc0b2da8079f591ced28c12f731fbf767e0aa1d7e0a4e67ceee8
|
|
| MD5 |
78787d79d44703dd1c2f362797f79850
|
|
| BLAKE2b-256 |
7b493fae3feac5df2cb6e25183a08faa691c23653ef30e1c4df7880d86cc8795
|
Provenance
The following attestation bundles were made for inversipy-0.1.0-py3-none-any.whl:
Publisher:
publish.yml on mottetm/inversipy
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
inversipy-0.1.0-py3-none-any.whl -
Subject digest:
f42c2988bbaecc0b2da8079f591ced28c12f731fbf767e0aa1d7e0a4e67ceee8 - Sigstore transparency entry: 1019271188
- Sigstore integration time:
-
Permalink:
mottetm/inversipy@c24a613c8b1429193cd53540e50f5f8a43307513 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/mottetm
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@c24a613c8b1429193cd53540e50f5f8a43307513 -
Trigger Event:
push
-
Statement type: