Typorithm · easy

Flask: query parameters and bounded pagination

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

Step 1 of 4 · intro card

Flask: query parameters and bounded pagination

0%

Concept and contract

Treat query strings as untrusted strings and bound each page.

Query parameters arrive as strings and may be absent, repeated or malformed. Pagination needs explicit defaults and upper bounds so one caller cannot request an entire dataset accidentally.

The fixture uses a fixed ordered collection of five identifiers, which makes ordering and truncation observable. In a database-backed endpoint the same policy must be applied before expensive materialization.

Mechanism and worked example

The worked route accepts an ASCII decimal limit between one and fifty, defaults to ten and slices an already small owned list..

The worked route accepts an ASCII decimal limit between one and fifty, defaults to ten and slices an already small owned list.

It rejects signs, decimal points and non-ASCII numerals according to this deliberately narrow contract. The test client supplies the query string through its structured argument, avoiding manual URL concatenation and preserving the distinction between transport encoding and business validation.

Complete worked example

python

from flask import Flask, request
app = Flask(__name__)
app.config['TESTING'] = True
@app.get('/roles')
def listing():
    raw = request.args.get('limit', '10')
    if not raw.isascii() or not raw.isdecimal():
        return {'error': 'limit invalid'}, 400
    limit = int(raw)
    if not 1 <= limit <= 50:
        return {'error': 'limit invalid'}, 400
    return {'ids': list(range(1, 6))[:limit], 'limit': limit}

def page(query):
    response = app.test_client().get('/roles', query_string=query)
    return {'status': response.status_code, 'body': response.json}
Reasoning
The worked route accepts an ASCII decimal limit between one and fifty, defaults to ten and slices an already small owned list.

Counterexamples and common mistakes

The counterexample removes the upper bound.

The counterexample removes the upper bound. A limit of fifty-one then succeeds, violating the public resource policy even though the tiny fixture cannot exhaust memory.

That distinction matters: a test should assert the declared budget, not wait for a performance disaster. Silently coercing malformed values to zero also makes client mistakes difficult to diagnose and can change pagination semantics.

Counterexample: a runnable framework mistake

python

from flask import Flask, request
app = Flask(__name__)
app.config['TESTING'] = True
@app.get('/roles')
def listing():
    raw = request.args.get('limit', '10')
    if not raw.isascii() or not raw.isdecimal():
        return {'error': 'limit invalid'}, 400
    limit = int(raw)
    if not 1 <= limit:
        return {'error': 'limit invalid'}, 400
    return {'ids': list(range(1, 6))[:limit], 'limit': limit}

def page(query):
    response = app.test_client().get('/roles', query_string=query)
    return {'status': response.status_code, 'body': response.json}
Reasoning
The counterexample removes the upper bound. A limit of fifty-one then succeeds, violating the public resource policy even though the tiny fixture cannot exhaust memory.

Independent practice: one bounded framework change

Add an offset parameter with a default of zero and a maximum of one hundred.

Add an offset parameter with a default of zero and a maximum of one hundred. Validate it using the same ASCII decimal rule, then slice the ordered collection from offset to offset plus limit.

Preserve the existing limit contract. Empty later pages should be successful empty lists; an invalid offset should be rejected before attempting to build a result.

Step 1: Add a validated offset from 0 through 100. Step 2: Parse the default string zero.

Practice starter: solve before reading the solution

python

from flask import Flask, request
app = Flask(__name__)
app.config['TESTING'] = True
@app.get('/roles')
def listing():
    raw = request.args.get('limit', '10')
    if not raw.isascii() or not raw.isdecimal():
        return {'error': 'limit invalid'}, 400
    limit = int(raw)
    if not 1 <= limit <= 50:
        return {'error': 'limit invalid'}, 400
    return {'ids': list(range(1, 6))[:limit], 'limit': limit}

def page(query):
    # Remove this placeholder, then adapt the retained baseline:
    # Add a validated offset from 0 through 100.
    raise NotImplementedError
    response = app.test_client().get('/roles', query_string=query)
    return {'status': response.status_code, 'body': response.json}
Reasoning
Add a validated offset from 0 through 100. Parse the default string zero.
  • Hint 1: Validate before converting and slicing.
  • Hint 2: Use offset:offset + limit with stable ordering.

Solution, tests and trade-offs

Baseline cases cover the default, a two-item page and an over-budget request.

Baseline cases cover the default, a two-item page and an over-budget request. Practice also checks a middle slice, an empty later page and a negative offset.

Verify exact identifiers rather than only result length: a wrong starting position can otherwise pass. These fixtures do not imply that large SQL OFFSET values are efficient; the chosen budget is intentionally small.

Practice solution and acceptance checks

python

from flask import Flask, request
app = Flask(__name__)
app.config['TESTING'] = True
@app.get('/roles')
def listing():
    raw = request.args.get('limit', '10')
    if not raw.isascii() or not raw.isdecimal():
        return {'error': 'limit invalid'}, 400
    limit = int(raw)
    if not 1 <= limit <= 50:
        return {'error': 'limit invalid'}, 400
    raw_offset = request.args.get('offset', '0')
    if not raw_offset.isascii() or not raw_offset.isdecimal() or int(raw_offset) > 100:
        return {'error': 'offset invalid'}, 400
    offset = int(raw_offset)
    return {'ids': list(range(1, 6))[offset:offset + limit], 'limit': limit}

def page(query):
    response = app.test_client().get('/roles', query_string=query)
    return {'status': response.status_code, 'body': response.json}
Reasoning
Baseline cases cover the default, a two-item page and an over-budget request. Practice also checks a middle slice, an empty later page and a negative offset.

Interview reasoning and next step

For a changing production collection, stable ordering and cursor semantics may be preferable to offset pagination.

For a changing production collection, stable ordering and cursor semantics may be preferable to offset pagination. A cursor should bind the ordering key and active filters, and authorization still applies before results are exposed.

Keep that extension separate from this lesson’s small query-parser exercise. The transferable habit is to define limits, validate before work and test boundary values explicitly.

Optional stretch — after the core task works: Design a cursor that includes the active filter identity.

Computer practice: https://applydjinn.com/typorithm/python-frameworks-lab-20261105.zip. Install the pinned dependencies in an isolated environment, run verify.py --track flask, then edit the retained exercise baseline and check it with --practice fl-pagination. The lab uses owned in-memory databases, local framework clients and tiny CPU tensors. It never reads the working ApplyDjinn database, contacts production feeds or downloads models.

Treat query strings as untrusted strings and bound each page.