Typorithm · medium

A dependency graph is cached within one request

pythonfastapiindependent-practice
Saved on this device
Full lesson · 4 learning steps Open any exercise for focused practice
1

Step 1 of 4 · intro card

A dependency graph is cached within one request

0%

Concept and contract

Observe reuse without mistaking it for a global cache.

Dependencies let a route declare the context it needs instead of constructing every input inside the endpoint. Several branches can depend on the same callable. FastAPI normally reuses that callable result within one request, which can avoid duplicated work and keep the request context consistent.

The example increments a synthetic counter when a dependency executes. The endpoint requests that dependency twice and returns both observed values. With default caching, both values match. A second HTTP request executes the dependency again; this is not a persistent application cache.

Mechanism and worked example

Depends identifies the dependency callable in each parameter.

Depends identifies the dependency callable in each parameter. The shared callable and its arguments participate in dependency resolution. The counter is enclosed in a fresh application fixture so its sequence is deterministic and unrelated tests cannot alter it.

The helper sends one or more local requests and returns their observed pairs plus the number of actual dependency calls. This distinguishes two useful properties: reuse inside a request and freshness across requests. Returning just the final HTTP status would prove neither.

Complete worked example

python

from typing import Annotated
from fastapi import Depends, FastAPI
from fastapi.testclient import TestClient

def inspect_example(requests):
    app = FastAPI()
    count = 0
    def observe():
        nonlocal count
        count += 1
        return count
    @app.get("/observe")
    def read(first: Annotated[int, Depends(observe)],
             second: Annotated[int, Depends(observe)]):
        return [first, second]
    with TestClient(app) as client:
        pairs = [client.get("/observe").json() for _ in range(requests)]
    return {"pairs": pairs, "calls": count}
Reasoning
Depends identifies the dependency callable in each parameter. The shared callable and its arguments participate in dependency resolution. The counter is enclosed in a fresh application fixture so its sequence is deterministic and unrelated tests cannot alter it. The helper sends one or more local requests and returns their observed pairs plus the number of actual dependency calls. This distinguishes two useful properties: reuse inside a request and freshness across requests. Returning just the final HTTP status would prove neither.

Focused decision: use the complete example for context

python

def observe():
    nonlocal count
    count += 1
    return count

@app.get("/observe")
def read(first: Annotated[int, Depends(observe)],
         second: Annotated[int, Depends(observe)]):
    return [first, second]
Focused decision
Reconstruct this focused decision after the warm-up. The complete imports, model or application, and test client remain in the worked example. Typing records practice; only the downloadable computer lab executes Python.

Counterexamples and common mistakes

The controlled mistake disables the cache on the second occurrence.

The controlled mistake disables the cache on the second occurrence. The two branches then observe different counter values and the call count rises. This is sometimes intentional, but it is a contract change that should be tested. Expensive work can also be duplicated accidentally if equivalent logic is wrapped in unrelated callables.

Do not use a request dependency cache as Redis, a connection pool or durable storage. Its lifetime is one request, and the counter itself is only a teaching instrument. A real shared resource needs explicit lifecycle and concurrency behavior rather than an assumption that Depends makes it globally safe.

Controlled mistake: observe the actual wrong behavior

python

from typing import Annotated
from fastapi import Depends, FastAPI
from fastapi.testclient import TestClient

def inspect_example(requests):
    app = FastAPI()
    count = 0
    def observe():
        nonlocal count
        count += 1
        return count
    @app.get("/observe")
    def read(first: Annotated[int, Depends(observe)],
             second: Annotated[int, Depends(observe, use_cache=False)]):
        return [first, second]
    with TestClient(app) as client:
        pairs = [client.get("/observe").json() for _ in range(requests)]
    return {"pairs": pairs, "calls": count}
Reasoning
The controlled mistake disables the cache on the second occurrence. The two branches then observe different counter values and the call count rises. This is sometimes intentional, but it is a contract change that should be tested. Expensive work can also be duplicated accidentally if equivalent logic is wrapped in unrelated callables. Do not use a request dependency cache as Redis, a connection pool or durable storage. Its lifetime is one request, and the counter itself is only a teaching instrument. A real shared resource needs explicit lifecycle and concurrency behavior rather than an assumption that Depends makes it globally safe.

Independent practice: one bounded change

Deliberately request a fresh second value by setting use_cache=False only on the second dependency.

Deliberately request a fresh second value by setting use_cache=False only on the second dependency. Keep the first occurrence cached. This small task turns the previous counterexample into a documented policy change and makes the resulting call counts explicit.

Check one request produces values one and two, and two requests continue with three and four. A zero-request fixture should perform no dependency work. Preserve a fresh application for every inspection; moving the counter to a module global would make the test depend on execution order.

Step 1: Request a fresh value only for the second dependency occurrence and verify the resulting calls. Step 2: Retain the working imports and application structure. Change the stated policy, run the supplied practice cases, then explain the newly accepted or rejected boundary.

Practice starter: solve before reading the solution

python

from typing import Annotated
from fastapi import Depends, FastAPI
from fastapi.testclient import TestClient

def inspect_example(requests):
    app = FastAPI()
    count = 0
    def observe():
        nonlocal count
        count += 1
        return count
    @app.get("/observe")
    def read(first: Annotated[int, Depends(observe)],
             second: Annotated[int, Depends(observe)]):
        return [first, second]
    with TestClient(app) as client:
        pairs = [client.get("/observe").json() for _ in range(requests)]
    return {"pairs": pairs, "calls": count}

# Independent task: Request a fresh value only for the second dependency occurrence and verify the resulting calls.
Reasoning
Request a fresh value only for the second dependency occurrence and verify the resulting calls.
  • Hint 1: Find the one field or branch that owns this policy.
  • Hint 2: Compare the first changed acceptance case with the retained baseline.

Solution, tests and trade-offs

The solution uses the documented cache switch at the intended occurrence.

The solution uses the documented cache switch at the intended occurrence. Each request now invokes the callable twice and the reported sequence confirms where the repeated work happened. The fixture does not change its output to hide the extra call or reset the counter between HTTP requests.

Before disabling caching in production, consider whether the dependency result represents one request principal, one snapshot or one transaction. Resolving it twice may produce inconsistent context. Conversely, a truly fresh observation might need repeated work. State the desired lifetime and verify it rather than treating all caching as an automatic improvement.

Practice solution and acceptance checks

python

from typing import Annotated
from fastapi import Depends, FastAPI
from fastapi.testclient import TestClient

def inspect_example(requests):
    app = FastAPI()
    count = 0
    def observe():
        nonlocal count
        count += 1
        return count
    @app.get("/observe")
    def read(first: Annotated[int, Depends(observe)],
             second: Annotated[int, Depends(observe, use_cache=False)]):
        return [first, second]
    with TestClient(app) as client:
        pairs = [client.get("/observe").json() for _ in range(requests)]
    return {"pairs": pairs, "calls": count}
Reasoning
The solution uses the documented cache switch at the intended occurrence. Each request now invokes the callable twice and the reported sequence confirms where the repeated work happened. The fixture does not change its output to hide the extra call or reset the counter between HTTP requests. Before disabling caching in production, consider whether the dependency result represents one request principal, one snapshot or one transaction. Resolving it twice may produce inconsistent context. Conversely, a truly fresh observation might need repeated work. State the desired lifetime and verify it rather than treating all caching as an automatic improvement.

Interview reasoning and next step

Explain the difference between reuse within a request and storage across requests using the two-request case.

Explain the difference between reuse within a request and storage across requests using the two-request case. Then describe what use_cache=False changes and which occurrence owns that choice. The observable sequence is a better explanation than saying dependencies are singletons.

Next, make failure responses explicit. Dependency failures and endpoint failures both need honest HTTP statuses. A convenient return value should never disguise an authorization or missing-resource failure as a successful operation.

Optional stretch — after the core task works: Add one independently reasoned boundary case and explain its expected value before running it.

Computer practice: https://applydjinn.com/typorithm/python-api-lab-20261105.zip. Use an isolated Python environment and the pinned dependencies. Run verify.py --track fastapi, then edit the working exercise baseline and run --practice fa-dependencies. The reference suite rejects meaningful counterexamples and checks values, types and unchanged inputs. All data is owned and all HTTP clients are in-process. No production database, feed, account or paid service is used.

Observe reuse without mistaking it for a global cache.