Simplifying Dependency Injection in Python Web Apps with PyNest

Itay
5 min readApr 15, 2024

--

Dependency Injection (DI) and Inversion of Control (IoC) are key design patterns that can significantly improve the structure and maintainability of web applications. They help in managing dependencies and controlling the flow of applications, making the code more modular and testable. PyNest is an open-source project that brings these concepts into the Python web development space, offering a practical approach to applying DI and IoC.

PyNest Dependency Injection System

Understanding DI and IoC

Dependency Injection is a technique where components receive their dependencies from an external source rather than creating them internally. This approach promotes a modular architecture, making the code easier to manage and test.

Inversion of Control, in addition to that, refers to transferring the control of objects or portions of a program to a container or framework. It’s about taking the control away from your objects to make them more decoupled and easier to manage.

Example

Imagine you’re building a web application with a service layer that needs to access a database. Without DI, the service might directly instantiate a database connection, making it tightly coupled to a specific database implementation.

class UserService:
def __init__(self):
self.db = Database() # Direct instantiation

def get_user(self, user_id):
return self.db.get_user(user_id)

With DI, the database connection (or a repository using it) is passed into the service as a parameter, making the service agnostic to the database implementation and easier to test.

class UserService:
def __init__(self, db):
self.db = db # Dependency injected, voila!

def get_user(self, user_id):
return self.db.get_user(user_id)

In this example, UserService doesn't need to know which database system it's using or how the database connection is established. This decouples the service from the database layer, allowing for greater flexibility and easier testing.

Another exited concept that makes the DI so powerful is the Singelton pattern.

Singleton Pattern in the Context of DI

The Singleton pattern ensures that a class has only one instance and provides a global point of access to it. This pattern can be particularly useful in web applications for managing resources that are expensive to create or need to be shared, such as database connections or configuration managers.

When combining the Singleton pattern with Dependency Injection, the DI framework typically manages the lifecycle of the singleton instance. This ensures that whenever a component requests a dependency that is defined as a singleton, the DI framework supplies the same instance.

Introducing PyNest

PyNest is a Python package that provides a straightforward way to implement DI and IoC in your web applications. It’s designed to be simple to use, focusing on reducing boilerplate code and making the development process more efficient.

PyNest is a Python framework that follows the modular architecture of NestJS

PyNest’s Approach to DI

PyNest uses decorators to enable DI, allowing developers to annotate their classes and functions. This makes it clear where dependencies are being injected, improving code readability and maintainability.

Practical Applications

Using PyNest can help refactor tightly coupled components in a web application into a more loosely coupled architecture. This not only facilitates easier unit testing but also improves the application’s adaptability to changing requirements.

Let’s imagen that we are building an application for managing library. We will focus on the Book resource.

The first component we will create is the Provider.

Injectable Providers

Providers are a broad category that includes services, repositories, factories, helpers, and more. They are typically used to encapsulate business logic, database access, or any functionality that might be shared across different parts of the application. In DI systems, providers are injected into consumers (like Controllers or other services) that need them.

In our case, the book service manages the database access —

from nest import Injectable
from providers.database import DatabaseRepository # DatabaseRepository is also an injectable that follows the repository pattern

@Injectable
class BookService:
def __init__(self, db: DatabaseRepository):
self.db = db # Inject Database into BookService

def get_book(self, book_id: str):
return self.db.get_book(book_id)

Here, BookService is marked as @Injectable, indicating that it can be a dependency that other parts of the application may require. The DI framework manages the lifecycle and dependencies of these providers.

The next part of our module will be the controller

Controllers

In the context of web applications, Controllers are responsible for handling incoming requests and returning responses to the client. They play a crucial role in managing the interaction between the user and the server. For a Python framework implementing DI, similar to how NestJS operates, Controllers would be defined classes where specific routes can trigger associated methods.

from nest import Controller, Get
from .book_service import BookService

@Controller('/users')
class BookController:

def __init__(self, book_service: BookService):
self.book_service = book_service

@Get('/:book_id')
def get_book(self, book_id: str):
return self.book_service.get_book(book_id)

In this example, the BookController handles requests related to books. The @Controller decorator helps define the routes at the class level, making the methods within the class respond to HTTP requests that match the given routes. In this example, the controller injects the book service from the DI framework.

The last part is the Module object —

Modules

Modules are an organizational framework that groups related components together. Each module is a cohesive block that encapsulates a certain set of capabilities and configuration. This architecture helps in maintaining a clear structure and promotes separation of concerns.

from nest.core import Module
from .book_controller import BookController
from .book_service import BookService
from providers.db import DatabaseRepository

@Module(
controllers: [BookController],
providers: [BookService, DatabaseRepository],
export: [BookService]
)
class UserModule:
pass

In the BookModule, both BookController and BookService are included. It is also included DatabaseRepositoryby declaring it in the provider's list. This module can be imported by the main application module, thereby organizing the codebase into a modular and manageable structure.

Visualizing PyNest’s DI Architecture

To bring our discussion to life, let’s visualize how PyNest manages dependencies with the help of the following architecture diagram:

This diagram encapsulates the dependency flow within a PyNest application, illustrating the modular approach and the decoupled nature of components facilitated by dependency injection (DI).

IoC Container

At the core, we have the IoC Container that uses the injectorpackacge for managing object creation and injecting dependencies where they are needed. for further reading — PynestContainer

Injector — The injector is a Python dependency injection framework, inspired by Guice, that aims to provide a Pythonic API with an emphasis on simplicity and support for static type checking. It avoids global state and encourages compartmentalized, decoupled code through the use of modules and injectable classes.

DI Framework

The DI Framework layer shows how @Injectable components like DBRepository and BookService are wired together. The Inject arrows highlight the direction of dependency resolution, ensuring that BookService does not have to know the specifics of the DBRepository.

--

--

Itay

Building Data Infrastructure @Lemonade | Ex-Meta | Author of PyNest - Python framework built on top of FastAPI that follows the modular architecture of NestJS