Typorithm · medium

Acquire and release application resources with lifespan

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

Step 1 of 4 · intro card

Acquire and release application resources with lifespan

0%

Concept and contract

Observe startup and cleanup by entering the TestClient context.

Some resources belong to the application lifetime rather than one request: a shared client, a pool or a loaded configuration snapshot. Constructing the FastAPI object does not prove those resources have been initialized. Use an explicit lifespan context to define acquisition and cleanup.

The fixture records open and close events and exposes a small readiness marker through a route. It uses only owned in-memory state. There is no actual database or remote client, so lifecycle behavior can be tested without contacting a service or modifying production data.

Mechanism and worked example

asynccontextmanager divides the lifespan into work before yield and cleanup after yield.

asynccontextmanager divides the lifespan into work before yield and cleanup after yield. The application stores its marker in state before serving the local requests. A finally block clears it and records closure even when the managed block exits through an exception.

The TestClient with block triggers lifespan entry and exit. After leaving that block, the helper reports the response observations, event trace and final closed state. This is stronger evidence than checking a successful request alone: it also shows that the resource was acquired once and released.

Complete worked example

python

from contextlib import asynccontextmanager
from fastapi import FastAPI
from fastapi.testclient import TestClient

def inspect_example(requests):
    events = []
    @asynccontextmanager
    async def lifespan(app):
        app.state.marker = "ready"
        events.append("open")
        try:
            yield
        finally:
            app.state.marker = None
            events.append("close")
    app = FastAPI(lifespan=lifespan)
    @app.get("/ready")
    def read():
        return {"marker": getattr(app.state, "marker", None)}
    with TestClient(app) as client:
        observed = [client.get("/ready").json() for _ in range(requests)]
    return {"observed": observed, "events": events,
            "closed": getattr(app.state, "marker", None) is None}
Reasoning
asynccontextmanager divides the lifespan into work before yield and cleanup after yield. The application stores its marker in state before serving the local requests. A finally block clears it and records closure even when the managed block exits through an exception. The TestClient with block triggers lifespan entry and exit. After leaving that block, the helper reports the response observations, event trace and final closed state. This is stronger evidence than checking a successful request alone: it also shows that the resource was acquired once and released.

Focused decision: use the complete example for context

python

@asynccontextmanager
async def lifespan(app):
    app.state.marker = "ready"
    try:
        yield
    finally:
        app.state.marker = None

app = FastAPI(lifespan=lifespan)
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 creates a TestClient without entering its context.

The controlled mistake creates a TestClient without entering its context. Requests can still reach an application in tests, but lifespan is not thereby guaranteed to run. The endpoint reports an uninitialized marker, and the expected open/close trace is absent.

Do not solve missing startup by lazily constructing a shared resource on every request without synchronization. That changes lifetime and failure behavior. Likewise, a cleanup function that is merely defined but never entered is not proof of release. Observe the actual trace through the intended application runner.

Controlled mistake: observe the actual wrong behavior

python

from contextlib import asynccontextmanager
from fastapi import FastAPI
from fastapi.testclient import TestClient

def inspect_example(requests):
    events = []
    @asynccontextmanager
    async def lifespan(app):
        app.state.marker = "ready"
        events.append("open")
        try:
            yield
        finally:
            app.state.marker = None
            events.append("close")
    app = FastAPI(lifespan=lifespan)
    @app.get("/ready")
    def read():
        return {"marker": getattr(app.state, "marker", None)}
    if (client := TestClient(app)):
        observed = [client.get("/ready").json() for _ in range(requests)]
    return {"observed": observed, "events": events,
            "closed": getattr(app.state, "marker", None) is None}
Reasoning
The controlled mistake creates a TestClient without entering its context. Requests can still reach an application in tests, but lifespan is not thereby guaranteed to run. The endpoint reports an uninitialized marker, and the expected open/close trace is absent. Do not solve missing startup by lazily constructing a shared resource on every request without synchronization. That changes lifetime and failure behavior. Likewise, a cleanup function that is merely defined but never entered is not proof of release. Observe the actual trace through the intended application runner.

Independent practice: one bounded change

Rename the ready marker to available while preserving open/close events and final cleanup.

Rename the ready marker to available while preserving open/close events and final cleanup. This deliberately small change requires finding the resource initialization point without altering the lifetime machinery. A zero-request client context should still acquire and release the resource.

Check zero, one and two requests. Both nonempty observations should use the new marker, and every inspection should finish closed. Keep the client with block and finally cleanup; changing only a returned string in the helper would hide whether the resource itself was initialized correctly.

Step 1: Change the initialized marker without changing acquisition, context entry or cleanup. 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 contextlib import asynccontextmanager
from fastapi import FastAPI
from fastapi.testclient import TestClient

def inspect_example(requests):
    events = []
    @asynccontextmanager
    async def lifespan(app):
        app.state.marker = "ready"
        events.append("open")
        try:
            yield
        finally:
            app.state.marker = None
            events.append("close")
    app = FastAPI(lifespan=lifespan)
    @app.get("/ready")
    def read():
        return {"marker": getattr(app.state, "marker", None)}
    with TestClient(app) as client:
        observed = [client.get("/ready").json() for _ in range(requests)]
    return {"observed": observed, "events": events,
            "closed": getattr(app.state, "marker", None) is None}

# Independent task: Change the initialized marker without changing acquisition, context entry or cleanup.
Reasoning
Change the initialized marker without changing acquisition, context entry or cleanup.
  • 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 changes the state marker set before yield.

The solution changes the state marker set before yield. Requests read it while the context is active, and the final result confirms it was cleared afterward. Startup and shutdown each occur once per client context, independent of the number of handled requests.

In production, acquisition can fail before the service is ready, and cleanup needs appropriate timeouts and failure handling. A process supervisor may eventually terminate a stuck process. This fixture demonstrates the ordering contract; it does not prove arbitrary external resources can always be closed successfully.

Practice solution and acceptance checks

python

from contextlib import asynccontextmanager
from fastapi import FastAPI
from fastapi.testclient import TestClient

def inspect_example(requests):
    events = []
    @asynccontextmanager
    async def lifespan(app):
        app.state.marker = "available"
        events.append("open")
        try:
            yield
        finally:
            app.state.marker = None
            events.append("close")
    app = FastAPI(lifespan=lifespan)
    @app.get("/ready")
    def read():
        return {"marker": getattr(app.state, "marker", None)}
    with TestClient(app) as client:
        observed = [client.get("/ready").json() for _ in range(requests)]
    return {"observed": observed, "events": events,
            "closed": getattr(app.state, "marker", None) is None}
Reasoning
The solution changes the state marker set before yield. Requests read it while the context is active, and the final result confirms it was cleared afterward. Startup and shutdown each occur once per client context, independent of the number of handled requests. In production, acquisition can fail before the service is ready, and cleanup needs appropriate timeouts and failure handling. A process supervisor may eventually terminate a stuck process. This fixture demonstrates the ordering contract; it does not prove arbitrary external resources can always be closed successfully.

Interview reasoning and next step

Explain why creating an app and creating a client are not equivalent to exercising lifespan.

Explain why creating an app and creating a client are not equivalent to exercising lifespan. Use the zero-request case to show that lifecycle and request count are separate. Identify why finally is useful without claiming it defeats every possible process failure.

Next, narrow the lifetime further to dependencies with yield. Application resources and request resources have different ownership. Streaming responses make the cleanup point particularly important because response production can continue after the endpoint returns.

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-lifespan. 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 startup and cleanup by entering the TestClient context.