Skip to main content

Dependency injection

Project description

Logo

CI Coverage status PyPI - version Python - versions License Documentation

Features

  • Use it everywhere: Use dependency injection in web servers, background tasks, console applications, Jupyter notebooks, tests, etc.
  • Lifetimes: Singleton (same instance per application), Scoped (same instance per HTTP request scope) and Transient (different instance per resolution).
  • FastAPI integration out of the box, and pluggable to any web framework.
  • Automatic resolution and disposal: Automatically resolve constructor parameters and manage async and non-async context managers. It's no longer your concern to know how to create or dispose services.
  • Clear design inspired by one of the most used and battle-tested DI libraries, adding async-native support, important features and good defaults.
  • Centralized configuration: Register all services in one place using a clean syntax, and without decorators.
  • ty and Pyright strict compliant.

Installation

uv add aspy-dependency-injection

✨ Quickstart with FastAPI

Inject services into async endpoints using Annotated[..., Inject()].

class EmailService:
    pass


class UserService:
    def __init__(self, email_service: EmailService) -> None:
        self.email_service = email_service
    
    async def create_user(self) -> None:
        pass


app = FastAPI()

@app.post("/users")
async def create_user(user_service: Annotated[UserService, Inject()]) -> None:
    await user_service.create_user()

services = ServiceCollection()
services.add_transient(EmailService)
services.add_transient(UserService)
services.configure_fastapi(app)

✨ Quickstart without FastAPI

Define your services and create a service provider.

class EmailService:
    pass


class UserService:
    def __init__(self, email_service: EmailService) -> None:
        self.email_service = email_service
    
    async def create_user(self) -> None:
        pass

    
services = ServiceCollection()
services.add_transient(EmailService)
services.add_transient(UserService)

async def main() -> None:
    async with services.build_service_provider() as service_provider:
        user_service = await service_provider.get_required_service(UserService)
        await user_service.create_user()


if __name__ == "__main__":
    asyncio.run(main())

If you want a scope per operation (e.g., per HTTP request or message from a queue), you can create a scope from the service provider:

async with service_provider.create_scope() as service_scope:
    user_service = await service_scope.get_required_service(UserService)
    await user_service.create_user()

🔄 Lifetimes

  • Transient: A new instance is created every time the service is requested. Examples: Services without state, workflows, repositories, service clients...
  • Singleton: The same instance is used every time the service is requested. Examples: Settings (pydantic-settings), machine learning models, database connection pools, caches.
  • Scoped: A new instance is created for each new scope, but the same instance is returned within the same scope. Examples: Database clients, unit of work.

🏭 Factories

Sometimes, you need to use a factory function to create a service. For example, you have settings (a connection string, database name, etc.) stored using the package pydantic-settings and you want to provide them to a service DatabaseClient to access a database.

class ApplicationSettings(BaseSettings):
    database_connection_string: str


class DatabaseClient:
    def __init__(self, connection_string: str) -> None:
        pass

In a real DatabaseClient implementation, you must use a sync or async context manager, i.e., you instance it with:

async with DatabaseClient(database_connection_string) as client:
    ...

And, if you want to reuse it, you create a factory function with yield:

async def create_database_client(application_settings: ApplicationSettings) -> AsyncGenerator[DatabaseClient]:
    async with DatabaseClient(application_settings.database_connection_string) as database_client:
        yield database_client

With that factory, you have to provide manually a singleton of ApplicationSettings, and to know if DatabaseClient implements a sync or async context manager, or neither. Apart from that, if you need a singleton or scoped instance of DatabaseClient, it's very complex to manage the disposal of the instance.

Then, why don't just return it? With this package, you just have this:

def inject_database_client(application_settings: ApplicationSettings) -> DatabaseClient:
    return DatabaseClient(
        connection_string=application_settings.database_connection_string
    )

services.add_transient(inject_database_client)

The factories can take as parameters other services registered. In this case, inject_database_client takes ApplicationSettings as a parameter, and the dependency injection mechanism will resolve it automatically.

🧪 Simplified testing

You can create a fixture in conftest.py that provides a ServiceProvider instance:

@pytest.fixture
async def service_provider() -> AsyncGenerator[ServiceProvider]:
    async with services.build_service_provider() as service_provider:
        yield service_provider

And then you can inject it into your tests and resolve the services.

async def test_create_user(service_provider: ServiceProvider) -> None:
    user_service = await service_provider.get_required_service(UserService)

📝 Interfaces & abstract classes

You can register a service by specifying both the service type (interface / abstract class) and the implementation type (concrete class). This is useful when you want to inject services using abstractions.

class NotificationService(ABC):
    async def send_notification(self, recipient: str, message: str) -> None:
        ...


class EmailService(NotificationService):
    @override
    async def send_notification(self, recipient: str, message: str) -> None:
        pass


class UserService:
    def __init__(self, notification_service: NotificationService) -> None:
        self.notification_service = notification_service

    async def create_user(self, email: str) -> None:
        await self.notification_service.send_notification(email, "Welcome to our service!")


services.add_transient(NotificationService, EmailService)

📚 Documentation

For more information, check out the documentation.

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

aspy_dependency_injection-0.5.0.tar.gz (20.2 kB view details)

Uploaded Source

Built Distribution

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

aspy_dependency_injection-0.5.0-py3-none-any.whl (36.8 kB view details)

Uploaded Python 3

File details

Details for the file aspy_dependency_injection-0.5.0.tar.gz.

File metadata

  • Download URL: aspy_dependency_injection-0.5.0.tar.gz
  • Upload date:
  • Size: 20.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.9.18 {"installer":{"name":"uv","version":"0.9.18","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for aspy_dependency_injection-0.5.0.tar.gz
Algorithm Hash digest
SHA256 e18c4f9cf02f27f62ecb1847fab840b72e3575128fc6c7c6f85c140f5b28758b
MD5 f5b2da271405beb5968b7278529d71b0
BLAKE2b-256 9cb4f5a6de9edb1796a463558b66148e1ab7856940f158aae6086fd6d1baa783

See more details on using hashes here.

File details

Details for the file aspy_dependency_injection-0.5.0-py3-none-any.whl.

File metadata

  • Download URL: aspy_dependency_injection-0.5.0-py3-none-any.whl
  • Upload date:
  • Size: 36.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.9.18 {"installer":{"name":"uv","version":"0.9.18","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for aspy_dependency_injection-0.5.0-py3-none-any.whl
Algorithm Hash digest
SHA256 e2c7dd018058ad41b26c411fbc84bd0ccb85f03fdf7ae8b9f2e4fdca0f2779c0
MD5 337bb0ca55b35ca6ab1bdcc3b446c2e1
BLAKE2b-256 c67a20712ed88b3e6e5467f406b3c53b791bbcb111a22a014cda5277053424f7

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