A Flexible Approach to Dependency Management
Introduction
In software design, dependency injection (DI) is a powerful principle that decouples classes from the concrete implementations of their dependencies. Instead of creating dependencies inside a class, we “inject” them from the outside. This makes our code more modular, testable, and flexible. While constructor injection is the most common approach—passing dependencies via the constructor—sometimes we prefer property injection. Property injection allows us to set dependencies through attributes (properties) of a class after the object is created. This can be especially useful when the dependency is optional or when you want more flexibility in configuring objects.

Let’s consider a case where we want to inject a CacheService into a ProductService class. With property injection, the ProductService class does not expect the CacheService in its constructor. Instead, we define a property (typically with a setter method) to accept the dependency later.
Here’s a Python example:
class CacheService:
def get(self, key):
return f"Value for {key}"
def set(self, key, value):
print(f"Stored {key} -> {value}")
class ProductService:
def __init__(self):
self._cache_service = None # Initially no dependency
@property
def cache_service(self):
return self._cache_service
@cache_service.setter
def cache_service(self, service):
self._cache_service = service
def get_product(self, product_id):
if self._cache_service:
value = self._cache_service.get(product_id)
return f"Fetched from cache: {value}"
else:
return f"Fetched from database for product {product_id}"
How it works:
- The
ProductServiceis created without needingCacheService. - Later, we can inject the dependency like this:
cache = CacheService() product_service = ProductService() # Inject dependency via property product_service.cache_service = cache print(product_service.get_product(“101”))

If the cache is provided, ProductService uses it. If not, it falls back to another logic (e.g., database fetch).
This style of injection has several benefits:
- Flexibility: You don’t need to supply the dependency at construction.
- Optional dependencies: A service can work with or without the dependency.
- Late binding: Dependencies can be injected at runtime, even after the object is created.
However, property injection can sometimes make dependencies less explicit compared to constructor injection, which lists all required services clearly. That’s why property injection is often used when dependencies are optional, secondary, or may change after initialization.

Conclusion
Property injection in Python provides a clean and flexible way to inject dependencies after an object has been constructed. By using properties and setter methods, you can assign services like CacheService to classes like ProductService without tightly coupling them. This approach is particularly helpful when the dependency is optional or configurable at runtime. While constructor injection remains the most explicit and common form, property injection complements it by offering adaptability—allowing developers to design software that is both modular and dynamic.