Skip to main content

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

  1. Validate early: Call container.validate() at application startup to catch configuration errors early

  2. Use scopes appropriately:

    • SINGLETON for expensive resources (database connections, caches)
    • TRANSIENT for stateful services (request handlers, commands)
    • REQUEST for request-scoped resources (in web applications)
  3. Organize with modules: Group related dependencies into modules with clear public interfaces

  4. Prefer constructor injection: Use type-annotated constructors for dependency injection

  5. Use interfaces: Register interfaces and bind to implementations for better testability

  6. Child containers for isolation: Use child containers for request-scoped or test-specific dependencies

  7. 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

inversipy-0.1.0.tar.gz (29.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

inversipy-0.1.0-py3-none-any.whl (28.8 kB view details)

Uploaded Python 3

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

Hashes for inversipy-0.1.0.tar.gz
Algorithm Hash digest
SHA256 22ad4bbb1b4cf7d34a48c9afc87d01be833f539583b98692e24c79512b3d0ab3
MD5 1a92c0a4c2dc4e3a421c34441601f4e7
BLAKE2b-256 a8bc05e28c81321ad7648f95e938170bdc32a16025110fe46516be197c301af0

See more details on using hashes here.

Provenance

The following attestation bundles were made for inversipy-0.1.0.tar.gz:

Publisher: publish.yml on mottetm/inversipy

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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

Hashes for inversipy-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 f42c2988bbaecc0b2da8079f591ced28c12f731fbf767e0aa1d7e0a4e67ceee8
MD5 78787d79d44703dd1c2f362797f79850
BLAKE2b-256 7b493fae3feac5df2cb6e25183a08faa691c23653ef30e1c4df7880d86cc8795

See more details on using hashes here.

Provenance

The following attestation bundles were made for inversipy-0.1.0-py3-none-any.whl:

Publisher: publish.yml on mottetm/inversipy

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page