FastDepends

repository·main·Indexed 19 days ago

https://github.com/lancetnik/fastdepends

A pure-Python dependency injection system extracted from FastAPI, stripped of HTTP-specific logic. It supports both sync and async functions using @inject and Depends, providing a lightweight toolkit for FastAPI-style DI in any Python application. Features include dependency overriding via Provider, custom field implementation through CustomField, and integration with Starlette.

Tokens
10.5K
Snippets
38
Records
44
Agent score
64%

What's inside fast-depends

  1. How nested dependencies and caching work

    main

    Dependencies in FastDepends can contain their own dependencies. You simply declare Depends requirements within the dependency function itself.

    Caching Behavior: By default, FastDepends caches dependency responses for the duration of one @inject call task. If multiple nested dependencies require the same sub-dependency, that sub-dependency is executed only once and its result is shared. However, different @inject calls will have different caches.

    To disable caching for a specific dependency and force it to execute every time it is requested, use Depends(..., cache=False).

    from fast_depends import inject, Depends
    
    def dependency_a():
        return "A"
    
    def dependency_b(a: str = Depends(dependency_a)):
        return f"B with {a}"
    
    @inject
    def main_function(b: str = Depends(dependency_b)):
        return b
    
    # To disable caching for dependency_a:
    # def dependency_b(a: str = Depends(dependency_a, cache=False)):
  2. When to use FastDepends vs other DI libraries

    main

    Use FastDepends if you want a very small, lightweight toolkit specifically designed to provide the ability to use FastAPI Depends and typecasting everywhere in your project.

    If your project requires more complex Dependency Injection (DI) features like IoC containers, advanced scoping, or complex lifecycle management, consider more featured alternatives like Dishka, DI, or Dependency Injector.

  3. How FastDepends works

    main

    FastDepends operates by shifting the heavy lifting to application startup time to ensure high performance during runtime. The process follows four main stages:

    1. Initialization Time: FastDepends inspects your functions and builds a dependency graph. It creates special Pydantic models where the function's expected arguments are defined as model fields.
    2. Runtime Argument Capture: When a function is called, FastDepends captures the incoming *args and **kwargs and uses them to initialize the function's representation models.
    3. Dependency Execution: FastDepends executes the function's dependencies, passing the model fields as arguments, and then calls the original function.
    4. Output Casting: Finally, FastDepends captures the function's output and casts it to the expected return type.

    Because the library primarily works with *args and **kwargs, it remains framework-agnostic and can be integrated into various business domains and technologies. Runtime performance is comparable to Pydantic's speed because the library mostly performs type casting on pre-built models.

  4. Dependencies type casting and return types

    main

    FastDepends performs type casting at two stages:

    1. At the dependency function output: The return value is cast to the type specified in the dependency's return annotation.
    2. At the injector input: The value is cast again to the type specified in the parameter annotation of the @injected function.

    Performance Note: Because the return type is cached, if a dependency is used in N different functions, the cached return value will be cast N times. To minimize overhead, ensure your type annotations are accurate and consistent.

    from fast_depends import inject, Depends
    
    def simple_dependency(a: int, b: int = 3) -> str:
        return a + b  # cast 'return' to str first time
    
    @inject
    def method(a: int, d: int = Depends(simple_dependency)):
        # cast 'd' to int second time
        return a + d
    
    assert method("1") == 5
  5. Avoid invalid argument ordering when using Annotated

    main

    Because Annotated dependencies are treated as part of the type hint, you must follow standard Python argument rules. Specifically, you cannot declare an argument without a default value after an argument that has a default value.

    Invalid Pattern

    # This will raise a Python SyntaxError
    def func(user_id: int | None = None, user: CurrentUser): ...

    Correct Patterns

    Option 1: Provide a default value for the Annotated argument

    def func(user_id: int | None = None, user: CurrentUser = None): ...

    Option 2: Use Field(...) to make the argument required via Annotated

    UserId = Annotated[int, Field(...)]  # Field(...) marks it as required
    
    def func(user_id: UserId, user: CurrentUser): ...
  6. Understand CustomField execution order and kwargs flow

    main

    Custom fields are processed sequentially from left to right based on their definition in the function signature.

    Crucially, the **kwargs passed to a subsequent CustomField.use() method is the dictionary returned by the previous field's use() method. This allows fields to chain transformations.

    Example of flow:

    @inject
    def func(field1 = Header(), field2 = Header()): ...

    In this case, field2 receives the **kwargs that were returned by field1.use().

  7. Use an instance's `__call__` method as a dependency

    main

    If you need to configure the dependency behavior before it is used, you can initialize the class first and then use its __call__ method as the dependency. This allows you to pass configuration parameters to the constructor while the dependency itself behaves like a callable object.

    from fastdepends import Depends
    
    class MyDependency:
        def __init__(self, prefix: str):
            self.prefix = prefix
    
        def __call__(self, value: str) -> str:
            return f"{self.prefix}_{value}"
    
    # Initialize with configuration
    instance = MyDependency(prefix="test")
    
    def my_endpoint(dep: str = Depends(instance)):
        return dep
  8. Use a custom `Provider` for scoped dependency overrides

    main

    If you want to avoid overriding dependencies globally (which affects all tests), you can instantiate a local Provider object. This allows you to control overrides within a specific scope or for specific function calls using the dependency_overrides_provider argument in the @inject decorator.

    Use the provider.scope(original, override) context manager to ensure the override is only active within that block.

    from typing import Annotated
    from fast_depends import Depends, Provider, inject
    
    # Create a local provider instead of using the global one
    provider = Provider()
    
    def abc_func() -> int:
        raise Exception("Original dependency called!")
    
    def real_func() -> int:
        return 1
    
    # Pass the custom provider to the @inject decorator
    @inject(dependency_overrides_provider=provider)
    def func(
        dependency: Annotated[int, Depends(abc_func)]
    ) -> int:
        return dependency
    
    # Use the scope context manager to apply the override locally
    with provider.scope(abc_func, real_func):
        assert func() == 1
  9. Integrate FastDepends with Starlette

    main

    Starlette handlers typically receive only a single request argument. To use FastDepends with Starlette, you must create a wrapper that unwraps the request object into keyword arguments (kwargs) so that dependency injection can function. This is achieved by wrapping the original handler with fast_depends.inject and a custom Starlette-specific wrapper.

    To implement this, you need to:

    1. Define a custom field (e.g., Path) to extract data from the request.
    2. Create a wrapper function (e.g., @wrap_starlette) that handles the Starlette request lifecycle and calls the injected handler.
    3. Apply @wrap_starlette and fast_depends.inject to your route handlers.
    from typing import Annotated
    from starlette.responses import PlainTextResponse
    from starlette.routing import Route
    from starlette.applications import Starlette
    from fast_depends import Depends, inject
    
    # 1. Define a Custom Field to extract from request
    class Path:
        def __init__(self, name: str):
            self.name = name
    
    # 2. Create the Starlette wrapper to unwrap 'request' into kwargs
    def wrap_starlette(handler):
        @inject
        async def wrapper(request, **kwargs):
            # Logic to extract path params from request and pass to handler
            # (Implementation details would go here to map request.path_params to kwargs)
            return await handler(request, **kwargs)
        return wrapper
    
    # 3. Usage with Dependencies and Annotated
    @wrap_starlette
    async def get_user(user_id: Annotated[int, Path("user_id")]):
        return f"user {user_id}"
    
    @wrap_starlette
    async def hello(user: str = Depends(get_user)):
        return PlainTextResponse(f"Hello, {user}!")
    
    app = Starlette(debug=True, routes=[
        Route("/{user_id}", hello)
    ])
  10. Use classmethods or staticmethods as dependencies

    main

    You can use @classmethod or @staticmethod as dependencies. This is particularly useful when implementing OOP patterns like the Strategy pattern where the logic is encapsulated within the class but doesn't require a specific instance state.

    from fastdepends import Depends
    
    class MyDependency:
        @classmethod
        def __call__(cls, value: str) -> str:
            return f"class_{value}"
    
    def my_endpoint(dep: str = Depends(MyDependency.__call__)):
        return dep
  11. Override dependencies using `fast_depends.dependency_provider`

    main

    To replace a dependency with a mock or alternative implementation during testing, use the global fast_depends.dependency_provider.dependency_overrides dictionary.

    Map the original dependency function as the key and the replacement function as the value. When fast_depends resolves dependencies, it will execute the override instead of the original function, preventing the original dependency (and its sub-dependencies) from running.

    from fast_depends import dependency_provider
    
    def original_dependency():
        # This might call an expensive external service
        return "real_user"
    
    def mock_dependency():
        # This returns a fixed value for testing
        return "mock_user"
    
    # Register the override
    dependency_provider.dependency_overrides[original_dependency] = mock_dependency
    
    # Now, any function using Depends(original_dependency) will receive "mock_user"
  12. Best practices for Annotated arguments

    main

    To avoid ambiguity between Python's positional argument parsing and FastDepends dependency injection, follow these recommendations:

    1. Avoid positional arguments for Annotated types: Do not rely on positional arguments when calling functions that use Annotated dependencies. If you call func(1) where func(user: CurrentUser, user_id: int = None) is defined, FastDepends may attempt to map the positional value to the first parameter (user), leading to errors.
    2. Use Named Arguments: Always use keyword arguments (e.g., func(user_id=1)) when calling functions with Annotated dependencies.
    3. Use pydantic.Field: The most robust way to define dependencies is to use pydantic.Field within Annotated for all parameters. This explicitly defines requirements and avoids misunderstanding between the developer, Python, and FastDepends.

    Recommended Pattern:

    def func(user_id: Annotated[int, Field(...)], user: CurrentUser): ...