Python Type Hints Meet Dependency Injection
I took some time recently to actually understand a pattern I had just been blindly copying and using in FastAPI.
from typing import Annotated
from fastapi import Depends
def get_user_service( service: Annotated[UserService, Depends()] ): ...What is actually going on here?
Is Annotated a type hint?
Is Depends() creating a UserService?
Is FastAPI inspecting the type hint?
It’s something I’ve just been using without a deep understanding of it. So I decided to spend a little time breaking it down.
First: What Is Annotated?
Consider a normal Python type hint:
def process_user(user: User):
...The annotation informs tools such as your IDE, type checker, and static analyzer:
useris expected to be aUser.
Python itself generally does not enforce that annotation at runtime.
Now consider this:
from typing import Annotateddef process_user(
user: Annotated[User, "some metadata"]
):
...The actual type is still User but now there is some additional information to it. Annotated was introduced as a way to attach context-specific metadata to an existing type without changing the underlying type. A tool that understands the metadata can use it. A tool that doesn't will simply treat the annotation as the original type. A framework such as FastAPI uses this to attach information about how a parameter should be handled.
Enter Depends
FastAPI has its own dependency injection system. Normally we might write a dependency like this:
def get_database():
return DatabaseConnection()And then tell FastAPI that an endpoint depends on it:
@app.get("/users")
def get_users(
database = Depends(get_database)
):
return database.get_users()When the request arrives, FastAPI sees:
Depends(get_database)This is essentially telling the framework to call get_database() before calling get_users() and give the results to the database parameter. FastAPI handles resolving that dependency as part of its request dependency graph. Dependencies can themselves have dependencies, allowing FastAPI to recursively resolve a tree of requirements.
With modern Python type annotations, however, we can write this more clearly:
@app.get("/users")
def get_users(
database: Annotated[DatabaseConnection, Depends(get_database)]
):
return database.get_users()Now we have separated two ideas.
The first:
DatabaseConnectionanswers “what kind of object is this?”
The second:
Depends(get_database)answers “How should FastAPI obtain it?”
Combine them:
Annotated[
DatabaseConnection,
Depends(get_database)
]and we have both type information and framework metadata in one annotation.
So What Does Annotated[ClassName, Depends()] Mean?
This is where I felt the deep understanding is especially beneficial. Suppose we have:
class UserService:
def get_user(self, user_id: int):
...And the endpoint looks like this:
@app.get("/users/{user_id}")
def get_user(
user_id: int,
service: Annotated[UserService, Depends()]
):
return service.get_user(user_id)Notice that we did not write Depends(UserService)
We wrote Depends()with no argument.
So what is FastAPI supposed to call?
Get Marcus T’s stories in your inbox
Join Medium for free to get updates from this writer.
FastAPI can infer the dependency from the type annotation. In this case, Annoted[UserService, Depends()] effectively tells FastAPI that this parameter has type UserService, and UserService itself is the dependency. FastAPI supports this shortcut for classes used as dependencies. When the dependency is the same class named by the type annotation, Depends() doesn't need the class repeated. FastAPI can call the class to construct the instance.
service: Annotated[
UserService,
Depends()
]means roughly the same thing as:
service: Annotated[
UserService,
Depends(UserService)
]FastAPI sees the Depends() metadata, looks at the annotated type, determines that UserService is the dependency, calls it, and injects the resulting instance. So our endpoint receives UserService() without constructing it itself. That’s the important part.
Why Not Just Do This?
@app.get("/users/{user_id}")
def get_user(user_id: int):
service = UserService()
return service.get_user(user_id)That’s perfectly reasonable, especially for small applications. But in this implementation, the endpoint is now responsible for two things:
HTTP request
↓
Endpoint
├── create UserService
└── use UserServiceThe endpoint doesn’t simply use a service. It also decides how that service gets created. That becomes more relevant in real-world applications.
Dependencies Usually Have Dependencies
Our UserService probably doesn't work alone. Perhaps it needs a repository:
class UserRepository:
def __init__(self, database: Database):
self.database = database
def get_by_id(self, user_id: int):
...UserService needs that repository
class UserService:
def __init__(self, repository: UserRepository):
self.repository = repository
def get_user(self, user_id: int):
return self.repository.get_by_id(user_id)Without dependency injection, the endpoint expands to this:
@app.get("/users/{user_id}")
def get_user(user_id: int):
database = Database()
repository = UserRepository(database)
service = UserService(repository)
return service.get_user(user_id)Now the endpoint has too many concerns. It’s doing a lot of wiring, and that’s not the endpoint’s job.
Let FastAPI Build the Dependency Graph
Instead, we can define each dependency separately.
def get_database() -> Database:
return Database()Then:
def get_repository(
database: Annotated[
Database,
Depends(get_database)
]
) -> UserRepository:Then:
def get_user_service(
repository: Annotated[
UserRepository,
Depends(get_repository)
]
) -> UserService: return UserService(repository)Finally:
@app.get("/users/{user_id}")
def get_user(
user_id: int,
service: Annotated[
UserService,
Depends(get_user_service)
]
):
return service.get_user(user_id)The endpoint no longer knows how the service is constructed. It only knows that it needs a UserService. FastAPI resolves the rest. Dependencies can declare their own sub-dependencies, and FastAPI resolves the resulting graph. This is something I have just been out of routine, but now I understand how it is dependency injection doing something useful.
OK, But How Does That Depends() Work Without an Argument?
Say you have a service with a constructor that describes its dependencies.
class UserService:
def __init__( self, repository: Annotated[ UserRepository, Depends() ] ):
self.repository = repositoryAnd the repository also describes its dependencies.
def __init__( self, database: Annotated[ Database, Depends() ] ):
self.database = databaseNow the endpoint can use the service like so:
@app.get("/users/{user_id}")
def get_user( user_id: int, service: Annotated[ UserService, Depends() ] ):
return service.get_user(user_id)We have effectively built a dependency graph using type annotations:
UserService
│
▼
UserRepository
│
▼
DatabaseEach class says what it needs. FastAPI figures out how to build it. It’s a powerful concept worth understanding before using.
The Big Payoff: Interfaces
In Python, an interface is a blueprint or contract that defines a set of behaviors (methods and properties) a class must provide, without specifying how those behaviors are implemented.
Revisiting our payment processor class from a previous article:
from abc import ABC, abstractmethod
from datetime import datetime
class PaymentProcessor(ABC):
"""
Abstract base class for payment processors.
Defines the interface all processors must implement.
"""
def __init__(self, merchant_id: str):
self.merchant_id = merchant_id
self._transaction_log: list[dict] = []
# Abstract methods — must be implemented by subclasses
@abstractmethod
def charge(self, amount: float, currency: str) -> bool:
"""Charge the given amount. Returns True on success."""
...An implementation can look like this:
class StripePaymentGateway(PaymentProcessor):
def charge( self, amount: float ) -> None:
print( f"Charging ${amount} with Stripe" )The service depends on the abstraction:
class OrderService:
def __init__( self, payment_processor: PaymentProcessor ):
self.payment_processor = payment_processor
def checkout( self, total: float ):
self.payment_processor.charge(total)This is good design. OrderService does not know about Stripe. It only knows about PaymentProcessor.
Provider Functions Connect Abstractions to Implementations
If we used:
gateway: Annotated[ PaymentGateway, Depends() ]What class would FastAPI instantiate. PaymentProcessor is abstract. We don’t want PaymentProcessor, we want StripePaymentProcessor. This is where an explicit provider function is useful.
We can define a provider function like so:
def get_payment_processor() -> PaymentGateway:
return StripePaymentProcessor()And use it like so:
def get_order_service(
processor: Annotated[
PaymentProcessor,
Depends(get_payment_processor)
] ) -> OrderService:
return OrderService(processor)
@app.post("/orders")
def create_order(
order: OrderCreate,
service: Annotated[
OrderService,
Depends(get_order_service)
] ):
return service.checkout(order.total)Now the dependency graph looks like this:
Endpoint
│
▼
OrderService
│
▼
PaymentGateway
│
▼
StripePaymentGatewayOrderService never mentions Stripe. That decision happens outside the service. There is a clear boundary between application logic and application configuration.
Type Hints Have Become More Than Documentation
Frameworks like FastAPI have increasingly made use of type information at runtime.
A signature like:
def create_order(
order: OrderCreate,
service: Annotated[
OrderService,
Depends(get_order_service)
]
):communicates several things simultaneously.
- To the developer:
serviceis anOrderService - To the IDE:
ProvideOrderServiceautocomplete - To a static type checker:
Validate howOrderServiceis used - And to the framework (FastAPI)
Resolve this parameter through the dependency injection system.
A lot of architectural information is packed into one function signature.
When Should You Use Annotated[..., Depends()]?
This is the question I had to ask myself. I didn’t have a deep understanding of this concept, but I was using it, and it worked, and I didn’t question it. But after exploring this more, I’ve concluded that this is most useful when FastAPI needs an actual application service or infrastructure dependency. I already had good habits for separation of concerns, but could I explain it like this? Hopefully, now you can.









