Tutorial 1: Basic Dependency Injection¶
This tutorial introduces the core concept of dependency injection with dioxide using a simple example.
What is Dependency Injection?¶
Dependency Injection means giving an object its dependencies from the outside, rather than having it create them itself. This makes code:
Easier to test - Replace real dependencies with test fakes
More flexible - Swap implementations without changing code
Loosely coupled - Components don’t know about each other’s internals
The Problem: Tight Coupling¶
Without dependency injection, code is tightly coupled:
class EmailService:
def __init__(self):
self.smtp_client = SMTPClient("smtp.gmail.com", 587) # Hard-coded!
def send(self, to: str, subject: str, body: str):
self.smtp_client.send_email(to, subject, body)
class UserService:
def __init__(self):
self.email = EmailService() # Creating its own dependency!
def register_user(self, email: str, name: str):
self.email.send(email, "Welcome!", f"Hello {name}")
Problems with this approach:
Hard to test - Can’t test
UserServicewithout sending real emailsHard to change - Switching email providers requires editing
EmailServiceHidden dependencies - No way to know what
UserServiceneeds without reading the code
The Solution: Dependency Injection¶
With dioxide, dependencies are injected via constructor parameters:
from dioxide import service
@service
class EmailService:
def send(self, to: str, subject: str, body: str):
print(f"📧 Sending email to {to}: {subject}")
@service
class UserService:
def __init__(self, email: EmailService): # Dependency injected!
self.email = email
def register_user(self, email_addr: str, name: str):
self.email.send(email_addr, "Welcome!", f"Hello {name}")
print(f"✅ User {name} registered")
How it works:
@servicedecorator registers classes with dioxide’s containerType hints (
email: EmailService) tell dioxide what to injectContainer automatically creates
EmailServiceand injects it intoUserService
Using the Services¶
Option 1: Auto-injection (Recommended)
Just import the container and scan your application:
from dioxide import container
# Scan the current module/package for @service classes
container.scan(__name__)
# Now just create instances - dependencies are auto-injected!
user_service = UserService()
# Use the service
user_service.register_user("alice@example.com", "Alice")
Output:
📧 Sending email to alice@example.com: Welcome!
✅ User Alice registered
Option 2: Explicit Resolution
You can also explicitly resolve services from the container:
from dioxide import Container
container = Container()
container.scan(__name__)
# Resolve UserService from container
user_service = container.resolve(UserService)
user_service.register_user("bob@example.com", "Bob")
# Or use bracket syntax
user_service = container[UserService]
Complete Example¶
Here’s a complete, runnable example:
"""
Basic Dependency Injection Example
This example demonstrates:
- @service decorator for automatic registration
- Constructor injection via type hints
- Automatic dependency resolution
"""
from dioxide import service, Container
@service
class Logger:
"""Simple logging service."""
def info(self, message: str) -> None:
print(f"ℹ️ INFO: {message}")
def error(self, message: str) -> None:
print(f"❌ ERROR: {message}")
@service
class EmailService:
"""Email sending service with logging."""
def __init__(self, logger: Logger):
"""Logger is automatically injected by dioxide."""
self.logger = logger
def send(self, to: str, subject: str, body: str) -> None:
"""Send an email (simulated)."""
self.logger.info(f"Sending email to {to}: {subject}")
# In real app, this would use SMTP, SendGrid, etc.
print(f"📧 To: {to}\n Subject: {subject}\n Body: {body}")
@service
class UserService:
"""User registration service."""
def __init__(self, email: EmailService, logger: Logger):
"""Both EmailService and Logger are auto-injected."""
self.email = email
self.logger = logger
def register_user(self, email_addr: str, name: str) -> None:
"""Register a new user and send welcome email."""
self.logger.info(f"Registering user: {name} ({email_addr})")
# Send welcome email
self.email.send(
to=email_addr,
subject="Welcome!",
body=f"Hello {name}, welcome to our platform!"
)
self.logger.info(f"User {name} registered successfully")
def main():
"""Run the example."""
print("=" * 70)
print("BASIC DEPENDENCY INJECTION EXAMPLE")
print("=" * 70)
# Set up the container
container = Container()
container.scan(__name__)
# Get UserService (with all dependencies auto-injected)
user_service = container.resolve(UserService)
# Use the service
user_service.register_user("alice@example.com", "Alice")
print()
user_service.register_user("bob@example.com", "Bob")
print("=" * 70)
print("KEY TAKEAWAYS:")
print("✅ Services depend on other services via type hints")
print("✅ No manual wiring - dioxide handles injection")
print("✅ Dependencies are explicit in constructor")
print("✅ Easy to understand what each service needs")
print("=" * 70)
if __name__ == "__main__":
main()
Running the Example¶
Save the example to a file (e.g., basic_di.py) and run it:
python basic_di.py
Expected Output:
======================================================================
BASIC DEPENDENCY INJECTION EXAMPLE
======================================================================
ℹ️ INFO: Registering user: Alice (alice@example.com)
ℹ️ INFO: Sending email to alice@example.com: Welcome!
📧 To: alice@example.com
Subject: Welcome!
Body: Hello Alice, welcome to our platform!
ℹ️ INFO: User Alice registered successfully
ℹ️ INFO: Registering user: Bob (bob@example.com)
ℹ️ INFO: Sending email to bob@example.com: Welcome!
📧 To: bob@example.com
Subject: Welcome!
Body: Hello Bob, welcome to our platform!
ℹ️ INFO: User Bob registered successfully
======================================================================
KEY TAKEAWAYS:
✅ Services depend on other services via type hints
✅ No manual wiring - dioxide handles injection
✅ Dependencies are explicit in constructor
✅ Easy to understand what each service needs
======================================================================
Key Concepts¶
@service Decorator¶
The @service decorator tells dioxide:
This class should be managed by the container
Create one instance (singleton) per container
Auto-inject dependencies based on constructor type hints
Singleton behavior: By default, @service classes are singletons. The container creates one instance and reuses it:
container = Container()
container.scan(__name__)
service1 = container.resolve(UserService)
service2 = container.resolve(UserService)
assert service1 is service2 # Same instance!
Type Hints for Injection¶
dioxide uses Python’s type hints to know what to inject:
@service
class UserService:
def __init__(self, email: EmailService): # Type hint here!
self.email = email
Important: dioxide only injects parameters that have type hints. Parameters without type hints must be provided manually:
@service
class UserService:
def __init__(self, email: EmailService, debug: bool = False):
self.email = email # Injected by dioxide
self.debug = debug # Must be provided manually
Container Scanning¶
container.scan(__name__) tells dioxide:
Look in the specified module/package
Find all classes decorated with
@service(or@adapter)Register them in the container
You can scan entire packages:
# Scan a specific package
container.scan("myapp.services")
# Scan multiple packages
container.scan("myapp.services")
container.scan("myapp.adapters")
Next Steps¶
This tutorial showed basic dependency injection without profiles or ports. In the next tutorial, we’ll learn:
Ports and Adapters - Define interfaces (ports) with multiple implementations (adapters)
Profiles - Use different implementations for production vs testing
Testing without Mocks - Use fast fakes instead of mocking frameworks
Continue to: Tutorial 2: Email Service with Profiles