Thanks to visit codestin.com
Credit goes to github.com

Skip to content

✨ Add lifespan dependency scope#12529

Open
UltimateLobster wants to merge 57 commits into
fastapi:masterfrom
UltimateLobster:feature/lifespan-scoped-dependencies
Open

✨ Add lifespan dependency scope#12529
UltimateLobster wants to merge 57 commits into
fastapi:masterfrom
UltimateLobster:feature/lifespan-scoped-dependencies

Conversation

@UltimateLobster

@UltimateLobster UltimateLobster commented Oct 24, 2024

Copy link
Copy Markdown
Contributor

Support for lifespan scoped dependencies

This feature adds a scope to Depends: "lifespan".

If it is "lifespan" the dependency will be evaluated at the application startup (the value will be reused in all endpoints), and will be teared down at the application shutdown. It will be useful when defining db connections which need to be maintained and reused throughout the application's up-time:

def mongo_client():
    with MongoClient(SOME_CONNECTION_STRING) as client:
        yield client

ActiveMongoClient = mongo_client: Annotated[MongoClient, Depends(mongo_client, scope="lifespan")]

# A single instance of MongoClient will be evaluated when the application starts. That same instance will be used for all
# requests to both endpoints. When the application shuts down, that single instance will be cleaned up as well.

@app.post("/users")
def insert_user(
    mongo_client: ActiveMongoClient,
    username: str,  
) -> str:
    inserted_id = mongo_client["SomeDb"]["Users"].insert_one({"username": username})
    return user_id

@app.get("/users/{user_id}/username")
def get_username(
    mongo_client: ActiveMongoClient,
    user_id: Annotated[str, Path()],
) -> str:
    user = mongo_client["SomeDb"]["Users"].find_one({"_id": user_id})
    return user["username"]

The use_cache argument works in the same way for dependencies with "lifespan" scope as it is for dependencies with "request" or "function" scope:

def mongo_client():
    with MongoClient(SOME_CONNECTION_STRING) as client:
        yield client

DedicatedMongoConnection = mongo_client: Annotated[MongoClient, Depends(
    mongo_client, 
    scope="lifespan",
    use_cache=False
)]

# Each of these endpoints would have their own, dedicated mongo client which be reused for all requests to its respective endpoint. So overall, 2 instances will be created when the application starts. Those same 2 will be cleaned when the application shuts down.

@app.post("/users")
def insert_user(
    mongo_client: DedicatedMongoConnection ,
    username: str,  
) -> str:
    inserted_id = mongo_client["SomeDb"]["Users"].insert_one({"username": username})
    return user_id

@app.get("/users/{user_id}/username")
def get_username(
    mongo_client: DedicatedMongoConnection ,
    user_id: Annotated[str, Path()],
) -> str:
    user = mongo_client["SomeDb"]["Users"].find_one({"_id": user_id})
    return user["username"]

Implementation details to note:

  • There is a new, internal lifespan passed to the application's main router, which will be responsible to evaluate the dependencies.
  • If the user already has specified a lifespan argument, it will be merged with the internal lifespan using the recently added _merge_lifespan_context utility
  • Starlette forbids the use of startup/shutdown events in combination with the lifespan argument (if a lifespan is supplied, the events will never be handled). Since this PR makes it so we always have a lifespan arugment passed, it would've meant the on_event, on_startup and on_shutdown features would break. I solved it by manually calling them in the application's internal lifespan.
  • The lifespan scoped dependencies are gathered by iterating over each of the application's routes recursively. I added a property to APIRoutes which exposes this information, and updated the implementation of get_flat_dependent to concat dependencies as well.

Things left to do:

  • There are still tests missing from this PR which I'm still working on (Specifically, I need to add tests to cover interactions with websockets, as well as interactions with dependency overrides).
  • I still need to add the necessary documentation and examples.

@svlandeg svlandeg added the feature New feature or request label Oct 25, 2024
UltimateLobster and others added 17 commits November 9, 2024 09:22
…ed to the deprecated startup and shutdown events. Fixed bugs related to dependency duplcatation within the same router scope. Made more specific dependency related exceptions. Fixed some linting and mypy related issues.
…imateLobster/fastapi into feature/lifespan-scoped-dependencies
…ests which check websocket-scoped teardowns before the teardown actually happened on the server side.
…imateLobster/fastapi into feature/lifespan-scoped-dependencies
@dolfinus

dolfinus commented Nov 10, 2024

Copy link
Copy Markdown
Contributor

You can just create client instance, and then pass it to application endpoints using dependency_overrides:

app = FastAPI()
client = ...
app.dependency_overrides[MongoClient] = lambda: client

@UltimateLobster

Copy link
Copy Markdown
Contributor Author

You can just create client instance, and then pass it to application endpoints using dependency_overrides:

app = FastAPI()
client = ...
app.dependency_overrides[MongoClient] = lambda: client

There are a few problems with this approach which this feature should be able to address:

  1. The client is never cleaned up at the server shutdown or on a reload.
  2. The creation of a resource during the import is problematic when you work with things like gunicorn, which may need to run the application in a separate process (There are clients which don't handle a fork of the connection object well).
  3. The creation may need to be asynchronous which further complicates the ability to do so in the global scope.
  4. Using dependency_overrides may lead to unexpected behavior when testing your application and one dependency overrides will take precedence over the other.
  5. Dependencies defined with use_cache=False will still use the exact same client (they should have a separate client just for them).
  6. When dealing with larger applications and lots of dependencies, it might be easy to forget to override the global dependency (since it's defined in a different place), and it will be a problem that might be harder to identify at the development stage since technically it will cause no errors.

Further discussion on the subject exists here.

@github-actions

Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot added the conflicts Automatically generated when a PR has a merge conflict label Oct 11, 2025
@github-actions

Copy link
Copy Markdown
Contributor

This pull request has a merge conflict that needs to be resolved.

@github-actions github-actions Bot removed the conflicts Automatically generated when a PR has a merge conflict label Oct 11, 2025
@github-actions github-actions Bot added the conflicts Automatically generated when a PR has a merge conflict label Oct 23, 2025
@github-actions

Copy link
Copy Markdown
Contributor

This pull request has a merge conflict that needs to be resolved.

@github-actions github-actions Bot removed the conflicts Automatically generated when a PR has a merge conflict label Nov 8, 2025
@UltimateLobster UltimateLobster changed the title Add dependency_scope arguments to Depends in order to support lifespan scoped dependencies. Add lifespan dependency scope Nov 8, 2025
@UltimateLobster

Copy link
Copy Markdown
Contributor Author

Updated the PR so it uses the new scope parameter added to Depends in version 0.121.0

@github-actions github-actions Bot added the conflicts Automatically generated when a PR has a merge conflict label Nov 8, 2025
@github-actions

github-actions Bot commented Nov 8, 2025

Copy link
Copy Markdown
Contributor

This pull request has a merge conflict that needs to be resolved.

@YuriiMotov YuriiMotov left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@UltimateLobster, thank you for work you've done! Looks like you've put a lot of efforts into it!

This feature requires quite a lot of changes. So, let's first wait for Sebastian to validate the general idea.

Comment on lines +55 to +61
## The `use_cache` argument

The `use_cache` argument works similarly to the way it worked with endpoint
scoped dependencies. Meaning, as **FastAPI** gathers lifespan scoped dependencies, it
will cache dependencies it already encountered before. However, you can disable
this behavior by passing `use_cache=False` to `Depends`. This will cause a new lifespan
dependency to be created for every endpoint/dependency/router where it shows up:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is not correct. For other dependency scopes, use_cache works for this request only. Here, use_cache has different meaning.
I think for this reason new parameter should be introduced instead of use_cache. And passing use_cache should raise warning as it's useless.

Actually, I'm not sure it's needed.
Do you have any particular use cases for this option?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess I meant to say it works the same way for the respective scope. The relationship between the request dependencies and the request cache, is the same as between lifespan dependencies and lifespan cache. I should've found a better way to explain this.

As for its usefulness, I imagined a case of someone wanting a dedicated evaluation of the dependency. The example I gave in the documentation was a dedicated connection that was only used for requests of a certain route (which would usually require to create a new dependency function for that route).

But I agree it might be somewhat of a weak reason to support use_cache=False (along with wanting to allow a behavior that a reader of Depends's signature might expect).

If you think it would be better to disallow use_cache=False until a better argument arises, I won't mind doing it .

@YuriiMotov YuriiMotov changed the title Add lifespan dependency scope ✨ Add lifespan dependency scope Nov 13, 2025
@UltimateLobster

Copy link
Copy Markdown
Contributor Author

@UltimateLobster, thank you for work you've done! Looks like you've put a lot of efforts into it!

This feature requires quite a lot of changes. So, let's first wait for Sebastian to validate the general idea.

Thanks for taking a look at this!
I'm available to do whatever changes needed so this feature may get in. So hopefully Sebastian will like it :)

@nidhishgajjar

This comment was marked as spam.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

conflicts Automatically generated when a PR has a merge conflict feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants