Multi-value Query Parameters with Flask
Tags: python, flask, webdev
Published: 2019-09-07 04:28:20

## Super Short Intro to Flask
Flask is a microframework for building Python applications. It can be used to build RESTful APIs.

### Framing the Problem
Often, RESTful APIs have GET routes for fetching documents. For example, `GET http://localhost:5000/users` will return a list of all user documents in its response. A general convention to filter the results of this request is to append a query string such as `GET http://localhost:5000/users?type=jedi&species=human`. This request will return all users that are of type Jedi and human. Additionally, a single query parameter could have more than one value like `GET http://localhost:5000/users?type=jedi&type=sith&species=human`. This request will return all users that are Jedi or Sith and human. (Other formats of providing multiple values to a single query parameter are used in practice but are not supported by Flask. One of these formats is `/users?type=jedi,sith`.) With Flask, query parameters are retrieved by accessing the `request.args` property within a route definition:

```python
@app.route('/users')
def fetch_users():
    query_params = request.args
    .
    .
    .
```

### The Problem
If the query string has multi-value parameters like `?type=jedi&type=sith&species=human`, then the value returned by `request.args.to_dict()` is:
```json
{
  "type": "jedi",
  "species": "human"
}
```

Wait, what happened to the second value for type? Flask by default assumes query parameters to be of single-value.

### The Solution
The default behavior can be modified by passing `flat=False` into `to_dict()` like `request.args.to_dict(flat=False)`. This returns all values for each parameter as a list, instead of only returning the first value. Unfortunately, parameters with only one value also have this behavior applied, which means the output of the above example is:

```json
{
  "type": ["jedi", "sith"],
  "species": ["human"]
}
```

At this point, all the values for each parameter are accessible, but it would be convenient if the parameters with only a single value are not converted to lists. We can implement this behavior with the following code:

(Notice, parts of the Flask implementation are intentially omitted for brevity.)
```python
from flask import Flask, request

def normalize_query_param(value):
    """
    Given a non-flattened query parameter value,
    and if the value is a list only containing 1 item,
    then the value is flattened.

    :param value: a value from a query parameter
    :return: a normalized query parameter value
    """
    return value if len(value) > 1 else value[0]


def normalize_query(params):
    """
    Converts query parameters from only containing one value for each parameter,
    to include parameters with multiple values as lists.

    :param params: a flask query parameters data structure
    :return: a dict of normalized query parameters
    """
    params_non_flat = params.to_dict(flat=False)
    return {k: normalize_query_param(v) for k, v in params_non_flat.items()}


@app.route('/users')
def fetch_users():
    query_params = normalize_query(request.args)
    .
    .
    .
```

Here, the query parameters are retrieved using `request.args.to_dict(flat=False)` — then we iterate through each parameter. If the parameter value contains more than one value, the unmodified list is returned to the parameter, otherwise, its single value is returned. The result for the following query string `?type=jedi&type=sith&species=human` will be:

```json
{
  "type": ["jedi", "sith"],
  "species": "human"
}
```

Now, all multi-value query parameters are represented by lists, while maintaining the expected original behavior for single-value query parameters.

I hope you learned something. Let me know what you think in the comments.

Prost!