Home >Backend Development >Python Tutorial >The impact of typing in Python
Python version 3.5 introduces "type hints" to make the code more readable and facilitate developers to understand each other's code.
In strongly typed languages such as Java and C, dependency inversion (DI - Dependency Inversion) is an important technology, but it is difficult to implement in weakly typed languages.
The core idea of dependency inversion is: classes should not rely on specific implementations, but on abstractions. Because abstractions (interfaces or abstract classes) are relatively stable contracts.
Bad example:
<code class="language-python">class GasStation: def fill_tank(car, amount): car.fill(amount)</code>
In this example, the gas station can only refuel cars. To make matters worse, since function fill_tank
has no defined type, any value may be passed in and the error will only be discovered at runtime.
Good example:
<code class="language-python">from typing import Protocol class Vehicle(Protocol): def fill(amount: int) -> None: ... class GasStation: def fill_tank(vehicle: Vehicle, amount: int) -> None: vehicle.fill(amount)</code>
In this example, first define the abstract class Vehicle
(using typing.Protocol
). The GasStation
function of fill_tank
no longer relies on a specific car class, but on the Vehicle
interface, thus becoming more general and can refuel any vehicle that implements the fill
method.
I took advantage of Python's type hinting system and created a library that simplifies the use of dependency inversion, called PyDIT (Python Dependency Injection with Types).
Suppose you need a database interface for storing user data. Whether you use PostgreSQL, MySQL, OracleDB, in-memory database or NoSQL database, you need to implement a database connection class and provide the functions of reading, writing and deleting records.
<code class="language-python">from time import sleep from typing import TypedDict from typing_extensions import override from uuid import UUID from src.configs.di import pydit from src.adapters.repositories.interfaces.user import UserRepository from src.constants.injection import MEMORY_REPOSITORY_CONFIG_TOKEN from src.domain.user.models.user import UserModel class ConfigType(TypedDict): delay: int class MemoryUserRepository(UserRepository): __users: dict[UUID, UserModel] = {} def __init__(self): self.__delay = self.config.get("delay", 0.2) @pydit.inject(token=MEMORY_REPOSITORY_CONFIG_TOKEN) def config(self) -> ConfigType: # TODO: supress return type error pass @override def get_by_id(self, *, id_: UUID) -> UserModel: sleep(self.__delay) user = self.__users.get(id_) if user is None: raise ValueError("User not found") return user @override def save(self, *, data: UserModel) -> None: sleep(self.__delay) self._check_pk_conflict(pk=data.id) self.__users[data.id] = data @override def list_(self) -> list[UserModel]: return list(self.__users.values()) def _check_pk_conflict(self, *, pk: UUID) -> None: if pk not in self.__users: return raise ValueError("Primary key conflicts: DB alrady has a user with this ID")</code>
In order to ensure that the code has nothing to do with database technology, define an interface that all database classes must follow:
<code class="language-python">from abc import abstractmethod from typing import Protocol from uuid import UUID from src.domain.user.models.user import UserModel class UserRepository(Protocol): @abstractmethod def get_by_id(self, *, id_: UUID) -> UserModel: pass @abstractmethod def save(self, *, data: UserModel) -> None: pass @abstractmethod def list_(self) -> list[UserModel]: pass</code>
Next, initialize the dependencies for injection:
<code class="language-python">from src.adapters.repositories.in_memory.user import MemoryUserRepository from src.constants.injection import MEMORY_REPOSITORY_CONFIG_TOKEN from .di import pydit from .get_db_config import get_db_config def setup_dependencies(): pydit.add_dependency(get_db_config, token=MEMORY_REPOSITORY_CONFIG_TOKEN) pydit.add_dependency(MemoryUserRepository, "UserRepository")</code>
Finally, inject the dependencies into the module that creates the user:
<code class="language-python">from typing import cast from src.adapters.repositories.interfaces.user import UserRepository from src.configs.di import pydit from src.domain.user.models.create_user import CreateUserModel from src.domain.user.models.user import UserModel from src.domain.user.services.create import CreateUserService from src.domain.user.services.list import ListUsersService class UserModule: @pydit.inject() def user_repository(self) -> UserRepository: return cast(UserRepository, None) def create(self, data: CreateUserModel) -> None: CreateUserService(self.user_repository).execute(data) def list_(self) -> list[UserModel]: return ListUsersService().execute()</code>
Dependencies are injected as properties and can be accessed via self
or module.user_repository
.
This example is simple, but PyDIT can be applied to a variety of project configurations, code abstractions, and SOLID principle scenarios. Welcome to try and contribute code!
Code repository: Github
LinkedIn: Marcelo Almeida (MrM4rc)
PyPI: python-pydit
The above is the detailed content of The impact of typing in Python. For more information, please follow other related articles on the PHP Chinese website!