Typorithm · medium

Filter before limiting and count the same scope

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

Step 1 of 4 · intro card

Filter before limiting and count the same scope

0%

Concept and contract

Keep country, stable order, page size and observed totals consistent.

A vacancy page is meaningful only when its rows and counts describe the same selection. Applying a limit before a country filter can discard matching roles that occur later in the source sequence. Counting the unfiltered source while displaying a country page creates a different misleading result.

The fixture contains four owned technical-role records across the United States, Israel and Ukraine. It is a tiny teaching collection, not a live feed or a claim about global job totals. The endpoint labels its total as observed and computes it after the requested country filter.

Mechanism and worked example

The route validates the country vocabulary, sorts by stable ID, selects matching records, then applies offset and limit.

The route validates the country vocabulary, sorts by stable ID, selects matching records, then applies offset and limit. The observed total is computed from the filtered collection before pagination. The returned items are therefore a page of that declared scope.

A small Pydantic response envelope records country, observed_total and items. Tests exercise a full page, a later offset and an invalid country. The local data intentionally starts with other countries so an incorrectly early limit cannot accidentally appear correct for every test.

Complete worked example

python

from typing import Annotated, Literal
from fastapi import FastAPI, Query
from fastapi.testclient import TestClient
from pydantic import BaseModel

class Role(BaseModel):
    id: int
    country: str
class Page(BaseModel):
    country: str
    observed_total: int
    items: list[Role]

def inspect_example(query):
    records = [{"id": 1, "country": "US"}, {"id": 2, "country": "IL"},
               {"id": 3, "country": "UA"}, {"id": 4, "country": "UA"}]
    app = FastAPI()
    @app.get("/roles", response_model=Page)
    def read(country: Literal["US", "IL", "UA"],
             limit: Annotated[int, Query(ge=1, le=2)] = 2,
             offset: Annotated[int, Query(ge=0)] = 0):
        ordered = sorted(records, key=lambda record: record["id"])
        selected = [record for record in ordered if record["country"] == country]
        return {"country": country, "observed_total": len(selected),
                "items": selected[offset:offset + limit]}
    with TestClient(app) as client:
        response = client.get("/roles", params=query)
        body = response.json()
        if response.status_code == 422:
            body = sorted({e["type"] for e in body["detail"]})
        return {"status": response.status_code, "body": body}
Reasoning
The route validates the country vocabulary, sorts by stable ID, selects matching records, then applies offset and limit. The observed total is computed from the filtered collection before pagination. The returned items are therefore a page of that declared scope. A small Pydantic response envelope records country, observed_total and items. Tests exercise a full page, a later offset and an invalid country. The local data intentionally starts with other countries so an incorrectly early limit cannot accidentally appear correct for every test.

Focused decision: use the complete example for context

python

ordered = sorted(records, key=lambda record: record["id"])
selected = [record for record in ordered if record["country"] == country]
page = selected[offset:offset + limit]
return {"country": country, "observed_total": len(selected), "items": page}
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 counterexample slices the ordered source before selecting the country.

The counterexample slices the ordered source before selecting the country. A Ukrainian request then misses matching roles beyond that early slice. Simply returning fewer rows is not harmless when the page implies that the filtered collection was considered first.

Do not deduplicate by title alone: different employers or requisitions can share a title. Production ingestion needs stable source identity and a cross-source merge policy. This lesson owns distinct IDs and tests pagination order; it does not claim to implement a feed-independent vacancy identity system.

Controlled mistake: observe the actual wrong behavior

python

from typing import Annotated, Literal
from fastapi import FastAPI, Query
from fastapi.testclient import TestClient
from pydantic import BaseModel

class Role(BaseModel):
    id: int
    country: str
class Page(BaseModel):
    country: str
    observed_total: int
    items: list[Role]

def inspect_example(query):
    records = [{"id": 1, "country": "US"}, {"id": 2, "country": "IL"},
               {"id": 3, "country": "UA"}, {"id": 4, "country": "UA"}]
    app = FastAPI()
    @app.get("/roles", response_model=Page)
    def read(country: Literal["US", "IL", "UA"],
             limit: Annotated[int, Query(ge=1, le=2)] = 2,
             offset: Annotated[int, Query(ge=0)] = 0):
        ordered = sorted(records, key=lambda record: record["id"])
        selected = [record for record in ordered[:limit] if record["country"] == country]
        return {"country": country, "observed_total": len(selected),
                "items": selected[offset:offset + limit]}
    with TestClient(app) as client:
        response = client.get("/roles", params=query)
        body = response.json()
        if response.status_code == 422:
            body = sorted({e["type"] for e in body["detail"]})
        return {"status": response.status_code, "body": body}
Reasoning
The counterexample slices the ordered source before selecting the country. A Ukrainian request then misses matching roles beyond that early slice. Simply returning fewer rows is not harmless when the page implies that the filtered collection was considered first. Do not deduplicate by title alone: different employers or requisitions can share a title. Production ingestion needs stable source identity and a cross-source merge policy. This lesson owns distinct IDs and tests pagination order; it does not claim to implement a feed-independent vacancy identity system.

Independent practice: one bounded change

Change the default limit from two to one while retaining the allowed maximum of two.

Change the default limit from two to one while retaining the allowed maximum of two. A request omitting limit should now receive one item, but observed_total must still count all matching fixture records. Explicit limit=2 should remain accepted.

Check the default Ukrainian page, an explicit two-item page and a later offset. Preserve sorting and country validation. Do not reduce observed_total to the number of returned items; that would change its meaning from filtered observed collection to current page length.

Step 1: Default to one item without changing explicit limits, filtered totals or operation order. 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, Literal
from fastapi import FastAPI, Query
from fastapi.testclient import TestClient
from pydantic import BaseModel

class Role(BaseModel):
    id: int
    country: str
class Page(BaseModel):
    country: str
    observed_total: int
    items: list[Role]

def inspect_example(query):
    records = [{"id": 1, "country": "US"}, {"id": 2, "country": "IL"},
               {"id": 3, "country": "UA"}, {"id": 4, "country": "UA"}]
    app = FastAPI()
    @app.get("/roles", response_model=Page)
    def read(country: Literal["US", "IL", "UA"],
             limit: Annotated[int, Query(ge=1, le=2)] = 2,
             offset: Annotated[int, Query(ge=0)] = 0):
        ordered = sorted(records, key=lambda record: record["id"])
        selected = [record for record in ordered if record["country"] == country]
        return {"country": country, "observed_total": len(selected),
                "items": selected[offset:offset + limit]}
    with TestClient(app) as client:
        response = client.get("/roles", params=query)
        body = response.json()
        if response.status_code == 422:
            body = sorted({e["type"] for e in body["detail"]})
        return {"status": response.status_code, "body": body}

# Independent task: Default to one item without changing explicit limits, filtered totals or operation order.
Reasoning
Default to one item without changing explicit limits, filtered totals or operation order.
  • 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 query default only.

The solution changes the query default only. Filtering still precedes slicing, and the observed total remains two for both Ukrainian pages even when only one row is shown. A different offset changes the page, not the underlying filtered count.

A production implementation should apply equivalent predicates in its database or precomputed cache, use a stable snapshot when required and publish freshness and source scope. Offset pagination can shift as records change; cursor pagination is another design with its own ordering contract. Neither approach should silently mix a stale count with unrelated fresh rows.

Practice solution and acceptance checks

python

from typing import Annotated, Literal
from fastapi import FastAPI, Query
from fastapi.testclient import TestClient
from pydantic import BaseModel

class Role(BaseModel):
    id: int
    country: str
class Page(BaseModel):
    country: str
    observed_total: int
    items: list[Role]

def inspect_example(query):
    records = [{"id": 1, "country": "US"}, {"id": 2, "country": "IL"},
               {"id": 3, "country": "UA"}, {"id": 4, "country": "UA"}]
    app = FastAPI()
    @app.get("/roles", response_model=Page)
    def read(country: Literal["US", "IL", "UA"],
             limit: Annotated[int, Query(ge=1, le=2)] = 1,
             offset: Annotated[int, Query(ge=0)] = 0):
        ordered = sorted(records, key=lambda record: record["id"])
        selected = [record for record in ordered if record["country"] == country]
        return {"country": country, "observed_total": len(selected),
                "items": selected[offset:offset + limit]}
    with TestClient(app) as client:
        response = client.get("/roles", params=query)
        body = response.json()
        if response.status_code == 422:
            body = sorted({e["type"] for e in body["detail"]})
        return {"status": response.status_code, "body": body}
Reasoning
The solution changes the query default only. Filtering still precedes slicing, and the observed total remains two for both Ukrainian pages even when only one row is shown. A different offset changes the page, not the underlying filtered count. A production implementation should apply equivalent predicates in its database or precomputed cache, use a stable snapshot when required and publish freshness and source scope. Offset pagination can shift as records change; cursor pagination is another design with its own ordering contract. Neither approach should silently mix a stale count with unrelated fresh rows.

Interview reasoning and next step

Explain the operation order using the Ukrainian records that occur after the first source entries.

Explain the operation order using the Ukrainian records that occur after the first source entries. Then distinguish observed_total, page length and a global market claim. Tests should catch a count mismatch without needing to crawl a public feed.

Next, consider deferred work. Returning a successful acceptance response does not mean a task is durable or exactly-once. Keep those semantics as precise as the count scope on this page.

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-pagination. 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.

Keep country, stable order, page size and observed totals consistent.