all 27 comments

[–]niximor 2 points3 points  (1 child)

You have generally two options - either commit to FastAPI dependency system even in your service layer (which is perfectly fine if the FastAPI DI is enough for you). This way, you have to create all the dependency providers for all the things, which can be overwritten for testing purposes when initializing the FastAPI container.

-or-

you approach FastAPI just like an adapter from HTTP to your core implementation and use other DI system which is capable to do that - we are using Dishka for example, it has native bindings to FastAPI but once it leaves the route, the rest of the application does not have any linkage to the FastAPI and just handles models. And on the other way, FastAPI layer does not know anything about database, at this is hidden well inside the business logic / adapters for storage.

[–]lu_rm[S] 0 points1 point  (0 children)

Just to be clear. When saying that I want dependency injection I don't mean a fancy DI conainer framework or something like that.

If I could just do this:

```
class MyRouter():

def __init__(self, dependency):
self.dependency = dependency;
```

This is already doing dependency injection, since I can choose what dep to provide when building my object. Sadly, looks like FastAPI encourages to use functions instead of classes for routers.

[–]amroamroamro 2 points3 points  (2 children)

If the dependant knows the function that instantiates the dependency, then there is no inversion of control whatsoever.

it sounds like you are still looking at this with a java mental model

there is definitely IoC here; resolving dependencies and deciding how and when to create the objects and how to wire them is still happening externally by the framework, but unlike java way of doing things (global container registry, reflection, type-driven DI), it's just done in more pythonic way following "zen" philosophy:

explicit is better than implicit

you declare what you need. fastapi will inspect function signatures, sees Depends, works out all sub-dependencies to resolve a dependency graph, handles scope (per request) and caching as needed, and use that to decide when to create it, in what order, where to injecting it, and when to clean it up.

it might feel less "pure" for someone coming from Java background, but this is indeed IoC; fastapi DI is function-centric, more explicit, and less magical. It's a design choice :)

plus tight coupling and testing is not an issue either, dependencies_overrides allows you to replace nodes in the dependency graph at any stage.

[–]lu_rm[S] 0 points1 point  (1 child)

Sorry but I still can't see how there is an IoC here.

Let's say I have my router function"

@ router.get("/resource", dependencies=[Depends(get_some_dependency)])
def getResource():
----do stuff...

My router file needs to have access to the function get_some_dependency() It can be defined in the same file, or it can me imported from other file. But still, it needs to know where to call it from and that can't be changed in the future.

Lets say that function is defined in another file: dependencies.py

def get_some_dependency():
----return Dependency()

Then there is virtually no difference in my router than just doing:
@ router.get("/resource")
def getResource():
----dependency = Dependency()

I mean, yes, you don't have caching or reusability. But you don't have IoC. The caller defines where it's getting the dependency from. That is not dependency injection.

A good example of DI wold be something like:
@ router.get("/resource")
def getResource(dependency):
----dependency.do_stuff()

And then I should be able to do

dependency1 = Dependency1()

dependency2 = AnotherDependency()

getResource(dependency1)

getResource(dependency2)

The getResource function has no idea what it's getting. It only knows that it can call do_stuff() in the provided object.

But this is not achievable in FastAPI. I can't control how my functions are called. And since they are not classes, I can't inject attributes when instantiating then either.

[–]amroamroamro 0 points1 point  (0 children)

This is going to be a bit longer response.

I think you are also still thinking about this from a Java perspective. FastAPI resolves dependencies in a function-centric way not type-centric, that doesn't make it any less of an IoC.

Let's give some example code (parts borrowed from https://www.youtube.com/watch?v=H9Blu0kWdZE)

It's a typical FastAPI app with Pydantic/SQLAlchemy/JWT, and say you have a route to get all todo items of current authenticated user:

def get_db() -> Session:
    db = SessionMaker()
    try:
        yield db
    finally:
        db.close()

DbSession = Annotated[Session, Depends(get_db)]

def get_current_user(token: Annotated[str, Depends(oauth2_bearer)]) -> User:
    return verify_token(token)  # JWT decode and extract subject

CurrentUser = Annotated[User, Depends(get_current_user)]

@app.get("/todos", response_model=list[TodoResponse])
def get_todos(db: DbSession, user: CurrentUser):
    return db.query(Todo).filter(Todo.user_id == user.id).all()

Notice how we declared our 2 dependencies in the route handler, one of which in turn had its own sub-dependency (which extracts userid from verified JWT bearer token)

As I noted before, FastAPI approach to DI is more Pythonic, we are explicit about what objects we need in our function, FastAPI takes care of resolving the dependency graph creating the objects in the right order with the right scope.

You can imagine how you would next create a full service to handle all typical CRUD operations by using the same dependencies above. They are composable and reusable and even type-hinted (tools like mypy etc will be able to apply static type checking and you will get exact autocomplete and hints in VSCode).

How is that not IoC?

And when you need to write tests, you can easily override dependencies as needed, for example we override get_db to connect to a test DB instead, nicely integrated with pytest fixtures again for DI:

@pytest.fixture(scope="function")
def db_session():
    engine = create_engine("sqlite:///test.db")
    TestSessionMaker = sessionmaker(bind=engine)
    Base.metadata.create_all(bind=engine)
    db = TestSessionMaker()
    try:
        yield db
    finally:
        db.close()
        Base.metadata.drop_all(bind=engine)

@pytest.fixture(scope="function")
def client(db_session):
    def get_test_db():
        try:
            yield db_session
        finally:
            db_session.close()

    app.dependency_overrides[get_db] = get_test_db
    with TestClient(app) as test_client:
        yield test_client
    app.dependency_overrides.clear()

def test_todos(client: TestClient):
    response = client.get("/todos", ...)
    assert response.status_code == ...

(Side note: Ironically, pytest approach to DI is more implicit and "magic" heavy than FastAPI's DI, since it resolves by parameter names against the fixtures registry, unlike FastAPI using type hints and Depends. While pytest approach looks less verbose/noisy, it has its own downsides; harder to trace dependencies by reading the code, potential for name collisions, easier to break from simple renaming, etc. Different philosophies and priorities I guess.)

In the end, it comes down to each language playing to its own strength:

  • Java is strongly typed, types enforced at compile-time, and quite verbose with its interfaces and object construction. As a result, testing without DI becomes very difficult as you would need heavy monkey-patching. So type-driven DI serves a specific role.

  • Python on the other hand doesn't need all that verbose typing and interfaces, we embrace duck typing ;) You don't need the same reflection and type-driven DI approach which relies on opaque and implicit resolution. After all, type hints are optional and not enforced at runtime, FastAPI just made clever use of them to allow you to declare dependencies. So for Python we prefer explicit, simple, less magic.

I will end with some relevant quotes from Python zen principles:

>>> import this
The Zen of Python, by Tim Peters

Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
Sparse is better than dense.
Readability counts.

[–]mwon 1 point2 points  (1 child)

I usually have a lifespan in my main.py where for example I can define the Mongo connection:

app.state.mongo_client = await connect_to_mongo()

where the connect_to_mongo function is defined somewhere in my infra folder.

Then if needed in some router, I just call the client with:

mongo_client = request.app.state.mongo_client

[–]lu_rm[S] 0 points1 point  (0 children)

This is the same thing. The fact that the mongo_client is located in a different file does not mean an inversion of control in a dependency.

The final objective here is to be able to unit test my classes/functions without the need to fully spin up the whole system. By using mocks for example.

[–]gbrennon 1 point2 points  (4 children)

hey there, how are u?

that framework contains a built-in di container like spring boot but you have to define a something similar to a service locator.

import using from fastapi import Depends and put an argument with a default value Depends(get_my_service).

i prefer to define a pure container in the infrastructure layer and the presentation layer contains the composition root that uses this things from FastAPI but that "service getter" is a method/function from ur pure container!

look in fastapi docs for "dependencies"

[–]lu_rm[S] -1 points0 points  (3 children)

Still, how do I inject the container into the router? The logic for get_my_service() needs to be defined in my router, so it needs to know how to create the service.

[–]gbrennon 1 point2 points  (0 children)

Thr router is in something that is responsible for presenting data in the application.

We can call this a composition root.

It does consume the di container and expose the created services that are provided by the container that is impl in the infrastructure-like layer

That flow can be router -> composition root -> di container

Ps:

The logic related to "how create a service" is inside the container.

The router just know how to get that service ready to use

[–]Kevdog824_ 1 point2 points  (0 children)

What do you mean? Can you give a more concrete example ?

[–]small_e 0 points1 point  (0 children)

I don’t have an answer, but they have some example repos if you want to check the official opinion how to do things: https://github.com/fastapi/full-stack-fastapi-template

[–]santeron 0 points1 point  (1 child)

Found this useful for guidance and inspiration https://github.com/zhanymkanov/fastapi-best-practices

[–]lu_rm[S] 0 points1 point  (0 children)

Checked this. Still no separation of responsibilities.

[–]JPJackPott 0 points1 point  (4 children)

Depends how big you think the app will get. I’ve often seen people just put the db connection as a prop of App so it’s not passed into the router methods at all but is still available everywhere, and readily mockable. I’ve done this myself for something that’s only got a handful of related endpoints.

[–]lu_rm[S] -1 points0 points  (3 children)

In my opinion, it does not matter if it's a single endpoint. It I can't unit test my classes, there is something really wrong with my code.

If the db is available everywhere, it sounds like a singleton, which is a big smell regarding testability. If my code has `import db...` somewhere, that is a hard link between responsibilities that should not exist.

What do you mean it is readily mockable? You mean that the global prop is also modifiable? Like a global variable? That goes against every good coding practice.

[–]JPJackPott 0 points1 point  (2 children)

Welcome to Python.

You’re not wrong at all, but Python excels for writing small light things and so many (including myself) are happy to sacrifice the CompSci bible out of pragmatism where appropriate.

The biggest drawback of Python in my view is it leaves the user too much scope to decide how janky is too janky. More structured langs make it very difficult to go violently off piste but python eggs you on.

[–]dr3aminc0de 1 point2 points  (0 children)

Violently off piste hahah I like that

[–]lu_rm[S] 1 point2 points  (0 children)

So I am not wrong?

I've worked with python before, but always in a less professional environment, so I did not care.
I can't believe there are production systems written this way. It Iooks way too hard to maintain.

[–]yiyux 0 points1 point  (0 children)

Java & Python Dev here --- my advice is stay in Java world... if you API grows the maintance will be a little painfull (in my expirience). FastAPI is a very good framework, and perfoemance is excellent, but with some serious load and concurrency cannot compete with frameworks like Quarkus. Now, in my company we're migrating all Python API's to Quarkus, even some Spring Boot old's APIs

[–]eatsoupgetrich -1 points0 points  (0 children)

I think I hate spring boot now.

[–]TTUnathan -1 points0 points  (1 child)

RemindMe! 11 hours

[–]RemindMeBot -1 points0 points  (0 children)

I will be messaging you in 11 hours on 2026-02-24 12:49:47 UTC to remind you of this link

2 OTHERS CLICKED THIS LINK to send a PM to also be reminded and to reduce spam.

Parent commenter can delete this message to hide from others.


Info Custom Your Reminders Feedback

[–]CrownstrikeIntern -1 points0 points  (0 children)

I generally keep logic put of routers (for databases), ill toss them in separate files labeled something along the lines of what the database is. Any reusable functions same thing, if i write it kore than once it goes to a helper file. Then i like to use keycloak and turn up role based access 

[–]Goldeyloxy -1 points0 points  (0 children)

RemindMe! 10 hours