Skip to main content

Type-safe, production-ready dependency injection for Python 3.13+

This project has been archived.

The maintainers of this project have marked this project as archived. No new releases are expected.

Project description

PyInj - Production-Ready Dependency Injection

Python Version Type Checked License: MIT

A type-safe, production-ready dependency injection container for Python 3.13+ that provides:

  • 🚀 Thread-safe and async-safe resolution
  • O(1) performance for type lookups
  • 🔍 Circular dependency detection
  • 🧹 Automatic resource cleanup
  • 🛡️ Protocol-based type safety
  • 🏭 Metaclass auto-registration
  • 📦 Zero external dependencies

Quick Start

# Install with UV (recommended)
uv add pyinj

# Or with pip
pip install pyinj
from pyinj import Container, Token, Scope

# Create container
container = Container()

# Define token
DB_TOKEN = Token[Database]("database")

# Register provider
container.register(DB_TOKEN, create_database, Scope.SINGLETON)

# Resolve dependency
db = container.get(DB_TOKEN)

# Cleanup
await container.dispose()

Why PyInj?

Traditional DI libraries are over-engineered:

  • 20,000+ lines of code for simple dependency injection
  • Heavy frameworks with steep learning curves
  • Poor async support and race conditions
  • Memory leaks and thread safety issues

PyInj is different:

  • ~200 lines of pure Python - easy to understand and debug
  • Designed specifically for Python 3.13+ with no-GIL support
  • Production-tested patterns with comprehensive safety guarantees
  • Can be vendored directly or installed as a package

Core Features

1. Type-Safe Dependencies

from typing import Protocol, runtime_checkable
from pyinj import Container, Token

@runtime_checkable
class Logger(Protocol):
    def info(self, message: str) -> None: ...

class ConsoleLogger:
    def info(self, message: str) -> None:
        print(f"INFO: {message}")

container = Container()
logger_token = Token[Logger]("logger", protocol=Logger)
container.register(logger_token, ConsoleLogger, Scope.SINGLETON)

# Type-safe resolution
logger = container.get(logger_token)  # Type: Logger
logger.info("Hello, World!")

2. Automatic Dependency Injection

from pyinj import Injectable

class EmailService(metaclass=Injectable):
    __injectable__ = True
    __token_name__ = "email_service" 
    __scope__ = Scope.SINGLETON
    
    def __init__(self, logger: Logger):
        self.logger = logger
    
    def send_email(self, to: str, subject: str) -> None:
        self.logger.info(f"Sending email to {to}")

# Automatically registered and dependencies resolved!
email_service = container.get(Injectable.get_registry()[EmailService])

3. Async-Safe with Proper Cleanup

class DatabaseConnection:
    async def connect(self) -> None:
        print("Connecting to database...")
    
    async def aclose(self) -> None:
        print("Closing database connection...")

container.register(
    Token[DatabaseConnection]("db"), 
    DatabaseConnection, 
    Scope.SINGLETON
)

# Async resolution
db = await container.aget(Token[DatabaseConnection]("db"))
await db.connect()

# Automatic cleanup
await container.dispose()  # Safely closes all resources

4. Testing Made Easy

# Production setup
container.register(logger_token, ConsoleLogger)

# Test override
test_logger = Mock(spec=Logger)
container.override(logger_token, test_logger)

# Test your code
service = container.get(service_token)
service.do_something()

# Verify interactions
test_logger.info.assert_called_with("Expected message")

# Cleanup
container.clear_overrides()

Advanced Usage

Protocol-Based Resolution

# Resolve by protocol instead of token
@container.inject
def business_logic(logger: Logger, db: Database) -> str:
    logger.info("Processing business logic")
    return db.query("SELECT * FROM users")

# Dependencies automatically injected based on type hints
result = business_logic()

Multiple Scopes

from pyinj import Scope

# Singleton - one instance per container
container.register(config_token, load_config, Scope.SINGLETON)

# Transient - new instance every time  
container.register(request_token, create_request, Scope.TRANSIENT)

# Request/Session - scoped to request/session context
container.register(user_token, get_current_user, Scope.REQUEST)

Async Patterns

# Async providers
async def create_async_service() -> AsyncService:
    service = AsyncService()
    await service.initialize()
    return service

container.register(service_token, create_async_service, Scope.SINGLETON)

# Concurrent resolution with race condition protection
results = await asyncio.gather(*[
    container.aget(service_token) for _ in range(100)
])

# All results are the same instance (singleton)
assert all(r is results[0] for r in results)

Performance

PyInj is optimized for production workloads:

  • O(1) type lookups - Constant time resolution regardless of container size
  • Cached injection metadata - Function signatures parsed once at decoration time
  • Lock-free fast paths - Singletons use double-checked locking pattern
  • Memory efficient - Minimal overhead per registered dependency
# Benchmark: 1000 services registered
# Resolution time: ~0.0001ms (O(1) guaranteed)
# Memory overhead: ~500 bytes per service

Framework Integration

FastAPI

from fastapi import FastAPI, Depends
from pyinj import Container

app = FastAPI()
container = Container()

def get_service(container: Container = Depends(lambda: container)) -> MyService:
    return container.get(service_token)

@app.post("/users")
async def create_user(service: MyService = Depends(get_service)):
    return await service.create_user()

Django/Flask

# Django settings.py
from pyinj import Container

# Global container
DI_CONTAINER = Container()

# In views
def my_view(request):
    service = DI_CONTAINER.get(service_token)
    return service.handle_request(request)

CLI Applications

import click
from pyinj import Container

@click.command()
@click.pass_context
def cli(ctx):
    ctx.obj = Container()
    # Register services...

@cli.command()
@click.pass_context  
def process(ctx):
    container = ctx.obj
    service = container.get(service_token)
    service.process()

Error Handling

PyInj provides clear, actionable error messages:

# Circular dependency detection
Container Error: Cannot resolve token 'service_a':
  Resolution chain: service_a -> service_b -> service_a
  Cause: Circular dependency detected

# Missing provider
Container Error: Cannot resolve token 'missing_service':
  Resolution chain: root
  Cause: No provider registered for token 'missing_service'

# Type validation failure  
Container Error: Provider for token 'logger' returned <Mock>, expected <Logger>

Migration Guide

From dependency-injector

# Before (dependency-injector)
from dependency_injector import containers, providers

class Container(containers.DeclarativeContainer):
    config = providers.Configuration()
    logger = providers.Singleton(Logger)

# After (PyInj)
from pyinj import Container, Token, Scope

container = Container()
logger_token = Token[Logger]("logger")
container.register(logger_token, Logger, Scope.SINGLETON)

From injector

# Before (injector)  
from injector import Injector, inject, singleton

injector = Injector()
injector.binder.bind(Logger, to=ConsoleLogger, scope=singleton)

@inject
def my_function(logger: Logger) -> None: ...

# After (PyInj)
container = Container()
container.register(Token[Logger]("logger"), ConsoleLogger, Scope.SINGLETON)

@container.inject
def my_function(logger: Logger) -> None: ...

Development

# Clone repository
git clone <repo-url>
cd pyinj

# Install dependencies  
uv sync

# Run tests
pytest

# Type checking
basedpyright src/

# Format code
ruff format .

# Run all quality checks
ruff check . && basedpyright src/ && pytest

Contributing

  1. Fork the repository
  2. Create a feature branch (git checkout -b feature/amazing-feature)
  3. Make changes with tests
  4. Ensure all quality checks pass
  5. Submit a pull request

License

MIT License - see LICENSE file for details.

Why "PyInj"?

Py - Python-first design for modern Python 3.13+
Inj - Injection (Dependency Injection)

PyInj follows the philosophy that good software is simple software. We provide exactly what you need for dependency injection - nothing more, nothing less.


Ready to simplify your Python dependency injection?

uv add pyinj

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

pyinj-1.0.1b1.tar.gz (21.4 kB view details)

Uploaded Source

Built Distribution

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

pyinj-1.0.1b1-py3-none-any.whl (25.9 kB view details)

Uploaded Python 3

File details

Details for the file pyinj-1.0.1b1.tar.gz.

File metadata

  • Download URL: pyinj-1.0.1b1.tar.gz
  • Upload date:
  • Size: 21.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.8.14

File hashes

Hashes for pyinj-1.0.1b1.tar.gz
Algorithm Hash digest
SHA256 cf9de7825ab741fa06a30aea8ff326f1a8b89c9a1ae904882481a83dfad64222
MD5 216512e0246b4bf04ca68f49a80be619
BLAKE2b-256 2a19d5257c3e7b6cae429a935e43264641364d82087aacd18e2a81cedee364e8

See more details on using hashes here.

File details

Details for the file pyinj-1.0.1b1-py3-none-any.whl.

File metadata

  • Download URL: pyinj-1.0.1b1-py3-none-any.whl
  • Upload date:
  • Size: 25.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.8.14

File hashes

Hashes for pyinj-1.0.1b1-py3-none-any.whl
Algorithm Hash digest
SHA256 3eeb6bd86626aa24ac7908bb1a4373908f429f41caf04972358065b71c1b70ff
MD5 6eb0b7161d8c5f6491b75b3fe19021d8
BLAKE2b-256 27d2dce23e5fbc41ccf644ff6bde9faed2aa4923bf64abd073829dd819fb2b9d

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