Skip to main content

Koin-like lightweight dependency injection container for Python with type inference support

Project description

KotInjection

image image image image image Unit Tests

Koin-like Dependency Injection Container for Python

KotInjection is a lightweight DI (Dependency Injection) container library for Python, inspired by Kotlin's Koin. It features type inference-based automatic dependency resolution and Koin-style DSL syntax.

Features

  • Simple API - Intuitive DSL similar to Koin
  • Type Inference Support - Automatic dependency resolution using Python type hints
  • Lifecycle Management - Supports singleton and factory patterns
  • Eager Initialization - Optional created_at_start for singleton pre-loading (like Koin)
  • Lazy Injection - Class attribute lazy dependency injection (like Koin's by inject())
  • Context Isolation - Independent container instances for isolated DI contexts
  • Lightweight - Pure Python implementation with no external dependencies
  • Type Safe - Safe dependency management through type hints

Installation

pip install kotinjection

Quick Start

Basic Usage

from kotinjection import KotInjection, KotInjectionModule


# Define dependencies
class Database:
    def __init__(self):
        self.connection = "db://localhost"


class UserRepository:
    def __init__(self, db: Database):
        self.db = db

    def get_users(self):
        return f"Users from {self.db.connection}"


# Create a module
module = KotInjectionModule()
with module:
    module.single[Database](lambda: Database())
    module.single[UserRepository](
        lambda: UserRepository(db=module.get())
    )

# Initialize the DI container
KotInjection.start(modules=[module])

# Retrieve dependencies
repo = KotInjection.get[UserRepository]()
print(repo.get_users())  # "Users from db://localhost"

# Stop when done
KotInjection.stop()

Lifecycle Management

# Singleton (same instance is reused)
module.single[Database](lambda: Database())

# Singleton with type (auto-instantiated with dependency resolution)
module.single[IDatabase](PostgresDatabase)

# Factory (new instance created each time)
module.factory[RequestHandler](
    lambda: RequestHandler(repo=module.get())
)

# Factory with type (auto-instantiated)
module.factory[RequestHandler](RequestHandler)

Eager Initialization

By default, singletons are lazily initialized on first access. Use created_at_start=True to initialize at start() time.

# Definition level - specific singleton is eagerly initialized
module.single[Database](lambda: Database(), created_at_start=True)

# Module level - all singletons in this module are eagerly initialized
module = KotInjectionModule(created_at_start=True)
with module:
    module.single[Database](lambda: Database())  # Eager
    module.single[Cache](lambda: Cache(), created_at_start=False)  # Override: Lazy

Lazy Injection (Class Attributes)

Similar to Koin's by inject(), you can define dependencies as class attributes that are resolved lazily on first access.

from kotinjection import KotInjection

class MyService:
    # Dependency is resolved on first access, not at class definition time
    repository = KotInjection.inject[UserRepository]

    def get_users(self):
        return self.repository.get_users()

# Initialize after class definition
KotInjection.start(modules=[module])

# Dependency is resolved when accessed
service = MyService()
service.get_users()  # repository is resolved here

Context Isolation

Context Isolation provides independent DI container instances that are completely separate from the global container. Ideal for library development, multi-tenant applications, and test isolation.

Basic Usage

from kotinjection import KotInjectionCore, KotInjectionModule

# Create an isolated container instance
module = KotInjectionModule()
with module:
    module.single[MyService](lambda: MyService())

app = KotInjectionCore(modules=[module])

# Retrieve dependencies from the instance
service = app.get[MyService]()

Use Case 1: Library Development

Create libraries that don't conflict with the host application's DI.

from kotinjection import (
    KotInjectionCore,
    IsolatedKotInjectionComponent,
    KotInjectionModule
)

# Define a library-specific container
library_module = KotInjectionModule()
with library_module:
    library_module.single[LibraryRepository](lambda: LibraryRepository())

library_app = KotInjectionCore(modules=[library_module])


# Base class for library components
class LibraryComponent(IsolatedKotInjectionComponent):
    def get_app(self):
        return library_app


# Actual service class
class MyLibraryService(LibraryComponent):
    def __init__(self):
        # Get dependencies from the isolated container
        self.repository = self.get[LibraryRepository]()

    def do_something(self):
        return self.repository.fetch_data()

Use Case 2: Multi-Tenant

Each tenant can have an independent DI environment.

# Tenant 1's container
tenant1_app = KotInjectionCore(modules=[tenant1_module])

# Tenant 2's container
tenant2_app = KotInjectionCore(modules=[tenant2_module])

# Use independent dependencies for each tenant
tenant1_service = tenant1_app.get[Service]()
tenant2_service = tenant2_app.get[Service]()

Use Case 3: Test Isolation

Use an independent DI container for each test case.

import unittest
from kotinjection import KotInjectionCore


class TestMyService(unittest.TestCase):
    def test_with_isolated_container(self):
        # Test-specific isolated container
        with KotInjectionCore(modules=[test_module]) as app:
            service = app.get[MyService]()
            # Run tests...
        # Automatically cleaned up when exiting the context

API Reference

For complete API documentation, see API Reference.

Quick Reference

Class Description
KotInjection Global DI container API
KotInjectionCore Isolated container instance
KotInjectionModule Dependency definitions container
IsolatedKotInjectionComponent Base class for isolated components
create_inject Create inject proxy for isolated containers

Key Methods

# Global API
KotInjection.start(modules=[...])  # Initialize
KotInjection.get[Type]()  # Retrieve dependency (eager)
KotInjection.inject[Type]  # Lazy injection (class attribute)
KotInjection.stop()  # Cleanup

# Module Definition
module = KotInjectionModule(created_at_start=True)  # Eager init for all singletons
module.single[Type](factory)  # Singleton with factory (lazy by default)
module.single[Type](ImplType)  # Singleton with type (auto-instantiated)
module.single[Type](factory, created_at_start=True)  # Singleton (eager)
module.factory[Type](factory)  # Factory with factory function
module.factory[Type](ImplType)  # Factory with type (auto-instantiated)
module.get()  # Type inference in factories
module.get[Type]()  # Explicit type resolution (for third-party libs)

# Isolated Container
create_inject(app)  # Create inject proxy for isolated containers

Advanced Usage

Multiple Modules

# Database module
db_module = KotInjectionModule()
with db_module:
    db_module.single[Database](lambda: Database())
    db_module.single[CacheService](lambda: CacheService())

# Repository module
repo_module = KotInjectionModule()
with repo_module:
    repo_module.single[UserRepository](
        lambda: UserRepository(
            db=repo_module.get(),
            cache=repo_module.get()
        )
    )

# Initialize with all modules
KotInjection.start(modules=[db_module, repo_module])

Type Inference with Isolated Containers

# Create the app first
app = KotInjectionCore()

# Use module.get() in module definitions
module = KotInjectionModule()
with module:
    module.single[Repository](lambda: Repository())
    module.single[Service](lambda: Service(repo=module.get()))

# Load modules
app.load_modules([module])

# Retrieve dependencies
service = app.get[Service]()

Mixing Manual Instances with Type Inference

module.get() resolves dependencies based on sequential call order. When mixing manually instantiated objects with module.get(), use keyword arguments for clarity.

Pattern That Doesn't Work

When module.get() call order doesn't match parameter order, type inference fails:

class UserRepository:
    def __init__(self, redis: Redis, db: Database):
        ...

# module.get() returns Redis (index 0), but we want Database!
module.single[UserRepository](
    lambda: UserRepository(Redis(host="localhost"), module.get())
)

Recommended: Use Keyword Arguments

module.single[UserRepository](
    lambda: UserRepository(redis=Redis(host="localhost"), db=module.get())
)

Alternative: Use Index Parameter

Specify the parameter index explicitly with module.get(index):

# module.get(1) resolves the second parameter (Database)
module.single[UserRepository](
    lambda: UserRepository(Redis(host="localhost"), module.get(1))
)

Explicit Type Resolution with module.get[Type]()

When using third-party libraries in factory functions, DryRun mode may cause errors because module.get() returns a placeholder object during type discovery.

The Problem

from sqlalchemy import create_engine

class Config:
    DATABASE_URI = "postgresql://localhost/db"

class DatabaseClient:
    def __init__(self, config: Config):
        # create_engine expects a real string, but during DryRun
        # module.get() returns a DryRunPlaceholder!
        self.engine = create_engine(config.DATABASE_URI)

module = KotInjectionModule()
with module:
    module.single[Config](lambda: Config())
    module.single[DatabaseClient](
        lambda: DatabaseClient(module.get())  # Error during DryRun!
    )

The Solution: Use module.get[Type]()

Use explicit type specification to resolve the actual instance even during DryRun:

module = KotInjectionModule()
with module:
    module.single[Config](lambda: Config())
    # get[Config]() returns the actual Config instance, not a placeholder
    module.single[DatabaseClient](
        lambda: DatabaseClient(module.get[Config]())  # Works!
    )

When to Use

Syntax DryRun Behavior Use When
module.get() Returns DryRunPlaceholder Dependency is stored/passed without immediate use
module.get[Type]() Returns actual instance Value is used immediately (e.g., passed to third-party libraries)

Mixing Both Styles

You can mix module.get() and module.get[Type]() in the same factory:

class Service:
    def __init__(self, config: Config, db: Database):
        self.uri = config.DATABASE_URI  # Uses config immediately
        self.db = db  # Just stores reference

module.single[Service](lambda: Service(
    module.get[Config](),  # Explicit type - actual instance
    module.get()           # Type inference - placeholder OK
))

Comparison with Koin

Feature Koin (Kotlin) KotInjection (Python)
DSL Syntax single { }, factory { } module.single[T], module.factory[T]
Type Inference get() module.get()
Lazy Injection by inject() KotInjection.inject[T]
Context Isolation koinApplication { } KotInjectionCore()
Scope Management Multiple scopes Singleton/Factory only
Context Manager N/A with statement support

Development Guidelines

Running Tests

python -m unittest discover tests

Type Hints

All type hints are required for dependency resolution.

class MyService:
    def __init__(self, repo: Repository):  # Type hint required
        self.repo = repo

License

MIT License

Acknowledgements

This project is inspired by Kotlin's Koin.

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

kotinjection-0.0.8.tar.gz (28.5 kB view details)

Uploaded Source

Built Distribution

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

kotinjection-0.0.8-py3-none-any.whl (36.6 kB view details)

Uploaded Python 3

File details

Details for the file kotinjection-0.0.8.tar.gz.

File metadata

  • Download URL: kotinjection-0.0.8.tar.gz
  • Upload date:
  • Size: 28.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: poetry/2.2.1 CPython/3.13.9 Linux/6.11.0-1018-azure

File hashes

Hashes for kotinjection-0.0.8.tar.gz
Algorithm Hash digest
SHA256 0ed2b84ab6c0e3e30104288b242206fa903171c0d9273e29ab2e0a10709097dd
MD5 13bf9244faf2a96521af96b30beedb14
BLAKE2b-256 dee982dedcf88a412103db7b47c765ede186bcb20ad436c5eaee335c49b5aa99

See more details on using hashes here.

File details

Details for the file kotinjection-0.0.8-py3-none-any.whl.

File metadata

  • Download URL: kotinjection-0.0.8-py3-none-any.whl
  • Upload date:
  • Size: 36.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: poetry/2.2.1 CPython/3.13.9 Linux/6.11.0-1018-azure

File hashes

Hashes for kotinjection-0.0.8-py3-none-any.whl
Algorithm Hash digest
SHA256 fb3ba7e71af036a7f2e98ca7b059e034b7bba35d5c23a8b1a37bc872b81310de
MD5 4db7f68b38a5a5df7b46b3b7e8e239b1
BLAKE2b-256 864abf61a2bb52f780d1b6062c493d79e85db6a0b310fcf5dcb16f634d9bd3dd

See more details on using hashes here.

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