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.0.tar.gz (30.5 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.0-py3-none-any.whl (13.9 kB view details)

Uploaded Python 3

File details

Details for the file pyinj-1.0.0.tar.gz.

File metadata

  • Download URL: pyinj-1.0.0.tar.gz
  • Upload date:
  • Size: 30.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.8.0

File hashes

Hashes for pyinj-1.0.0.tar.gz
Algorithm Hash digest
SHA256 52caed2b471f3a32ea2879b74c57032344ff60b6df146dbd1ec1282fb739de0d
MD5 2f292457399ae045d3b99efa68b3e22c
BLAKE2b-256 afed8e23c37ce574773f2c8cd239c21a8e3b921ce7bccc68b83b0d073a7a35b4

See more details on using hashes here.

File details

Details for the file pyinj-1.0.0-py3-none-any.whl.

File metadata

  • Download URL: pyinj-1.0.0-py3-none-any.whl
  • Upload date:
  • Size: 13.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.8.0

File hashes

Hashes for pyinj-1.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 7b8f602f6a911dc39cd4913cca9771e11d3b350f19b610435939d6ba251e18ba
MD5 85ad0a3e1682ef01f05f14bede8b66cd
BLAKE2b-256 7e0f2478d492f3b3507c85350c48b49fe0a582ed1b3d32cadc94fd0e4c863302

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