Litestar Bug Fix Query Parameter Defaults To Empty Guide
Hey everyone! Today, we're diving into a tricky issue encountered in Litestar, a powerful Python framework for building APIs. We'll explore a bug where query parameters don't default to empty as expected, leading to validation errors and unexpected behavior. Let's break it down and see how we can tackle this!
Understanding the Issue
In Litestar, you might define a query parameter like this:
from litestar import get, Empty
from litestar.params import Parameter
from typing import Annotated, Optional
@get(path="/notes")
async def get_notes(
around: Annotated[
Optional[int], # Allow None as a valid type
Parameter(
title="Around ID",
description="Retrieve notes around this ID.",
),
] = None # Default to None instead of Empty
) -> list[dict]: # Assuming you have a Note schema
# Your logic here
return []
The goal here is to make the around
parameter optional. If the user doesn't provide it in the query, we want it to default to an empty state, allowing our function to handle the request gracefully. However, the initial approach using Empty
and EmptyType
might lead to unexpected behavior.
The Problem Unveiled
When using Empty
as a default, Litestar's documentation might incorrectly show the parameter as required. More critically, when a request arrives without the around
parameter, Litestar raises a ValidationException
, complaining about a missing required query parameter. This is not the intended behavior, as we want the parameter to be optional.
2025-07-27 14:07:02 ERROR litestar Uncaught exception (connection_type=http, path=/api/v1/users/@me/notes):
Traceback (most recent call last):
File "C:\Users\iyadf\Documents\projects\notes-api\.venv\Lib\site-packages\litestar\_kwargs\extractors.py", line 107, in extractor
key: data[alias] if alias in data else alias_defaults[alias] for alias, key in alias_and_key_tuples
~~~~~~~~~~~~~~^^^^^^^
KeyError: 'around'
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "C:\Users\iyadf\Documents\projects\notes-api\.venv\Lib\site-packages\litestar\middleware\_internal\exceptions\middleware.py", line 158, in __call__
await self.app(scope, receive, capture_response_started)
File "C:\Users\iyadf\Documents\projects\notes-api\.venv\Lib\site-packages\litestar\routes\http.py", line 81, in handle
response = await self._get_response_for_request(
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
scope=scope, request=request, route_handler=route_handler, parameter_model=parameter_model
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
)
^
File "C:\Users\iyadf\Documents\projects\notes-api\.venv\Lib\site-packages\litestar\routes\http.py", line 133, in _get_response_for_request
return await self._call_handler_function(
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
scope=scope, request=request, parameter_model=parameter_model, route_handler=route_handler
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
)
^
File "C:\Users\iyadf\Documents\projects\notes-api\.venv\Lib\site-packages\litestar\routes\http.py", line 163, in _call_handler_function
kwargs = await parameter_model.to_kwargs(connection=request)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "C:\Users\iyadf\Documents\projects\notes-api\.venv\Lib\site-packages\litestar\_kwargs\kwargs_model.py", line 380, in to_kwargs
await extractor(output, connection)
File "C:\Users\iyadf\Documents\projects\notes-api\.venv\Lib\site-packages\litestar\_kwargs\extractors.py", line 116, in extractor
raise ValidationException(
f"Missing required {param.param_type.value} parameter {param.field_alias!r} for path {path}"
) from e
litestar.exceptions.http_exceptions.ValidationException: 400: Missing required query parameter 'around' for path /api/v1/users/@me/notes
This traceback clearly shows a KeyError
and a subsequent ValidationException
, indicating that Litestar expects the around
parameter to be present in the query.
Why is this happening, guys?
The issue lies in how Litestar interprets Empty
in this context. It doesn't treat it as a true default value in the same way it would treat None
or a specific value. Instead, it sees the parameter as required but without a default, hence the validation error.
Steps to Reproduce
To see this in action, you can follow these steps:
- Define a route handler using
Empty
as the default for a query parameter (as shown in the MCVE). - Send a request to that route without including the query parameter.
- Observe the
ValidationException
in the logs.
Visual Confirmation
The screenshot provided visually confirms that the documentation incorrectly marks the around
parameter as required, reinforcing the issue.
The Solution: Embracing Optional
and None
So, how do we fix this? The key is to use Optional[int]
from the typing
module and set the default value to None
. This tells Litestar that the parameter is optional and that None
is an acceptable default.
Here’s the corrected code:
from litestar import get
from litestar.params import Parameter
from typing import Annotated, Optional
@get(path="/notes")
async def get_notes(
around: Annotated[
Optional[int], # Allow None as a valid type
Parameter(
title="Around ID",
description="Retrieve notes around this ID.",
),
] = None # Default to None instead of Empty
) -> list[dict]: # Assuming you have a Note schema
# Your logic here
return []
Why does this work?
By using Optional[int]
, we explicitly tell Litestar that the around
parameter can be either an integer or None
. Setting the default value to None
ensures that if the parameter is not provided in the query, it will default to None
, and the validation will pass. Within your function, you can then check if around
is None
and handle the logic accordingly.
Diving Deeper: How Litestar Handles Parameters
To truly understand why this solution works, let's delve into how Litestar handles parameters. When a request comes in, Litestar's parameter extraction mechanism kicks in. It looks at the function signature, including the type annotations and default values, to determine how to extract and validate the parameters from the request.
When you use Annotated
with Parameter
, you're providing additional metadata about the parameter, such as its title and description. However, the core behavior of making a parameter optional or required is still governed by the type annotation and the default value.
The Role of Type Annotations
The Optional[int]
type annotation is crucial. It's equivalent to Union[int, None]
, meaning the parameter can be either an integer or None
. This is a standard Python typing construct that Litestar respects.
The Power of Default Values
The default value plays a vital role. When you set around: Optional[int] = None
, you're telling Litestar: "If the around
parameter is not provided in the request, treat it as None
." This is exactly the behavior we want for an optional query parameter.
The Pitfalls of Empty
Using Empty
as a default, while seemingly intuitive, doesn't convey the same meaning to Litestar. Empty
is more of a sentinel value used internally by Litestar to represent the absence of a value, rather than a valid default for an optional parameter. This is why it leads to validation errors.
Best Practices for Query Parameters in Litestar
To avoid similar issues in the future, here are some best practices to keep in mind when defining query parameters in Litestar:
- Use
Optional
for Optional Parameters: Always useOptional[YourType]
when you want a query parameter to be optional. - Default to
None
: Set the default value of optional parameters toNone
. - Handle
None
in Your Function: Inside your route handler, explicitly check forNone
values and handle them appropriately. - Leverage
Parameter
for Metadata: UseAnnotated
withParameter
to add titles, descriptions, and other metadata to your parameters for better documentation and clarity.
By following these practices, you can ensure that your query parameters behave as expected and your Litestar APIs are robust and user-friendly.
Practical Examples and Use Cases
Let's look at some practical examples of how you might use optional query parameters in your Litestar applications.
1. Filtering and Pagination
Imagine you're building an API for a blog. You might want to allow users to filter posts by category and paginate the results. Here’s how you could define the route:
from litestar import get
from litestar.params import Parameter
from typing import Annotated, Optional
@get(path="/posts")
async def get_posts(
category: Annotated[
Optional[str],
Parameter(title="Category", description="Filter posts by category"),
] = None,
page: Annotated[
Optional[int],
Parameter(title="Page", description="Page number for pagination"),
] = 1,
per_page: Annotated[
Optional[int],
Parameter(title="Per Page", description="Number of posts per page"),
] = 10,
) -> list[dict]:
# Your logic here to filter and paginate posts
# Use category, page, and per_page parameters
return []
In this example, category
is an optional string parameter, while page
and per_page
are optional integer parameters with default values of 1 and 10, respectively. This allows users to fetch all posts, filter by category, and paginate the results as needed.
2. Searching with Optional Keywords
Consider an API for a product catalog. You might want to allow users to search for products using optional keywords.
from litestar import get
from litestar.params import Parameter
from typing import Annotated, Optional
@get(path="/products/search")
async def search_products(
query: Annotated[
Optional[str],
Parameter(title="Query", description="Search keywords"),
] = None,
min_price: Annotated[
Optional[float],
Parameter(title="Min Price", description="Minimum price"),
] = None,
max_price: Annotated[
Optional[float],
Parameter(title="Max Price", description="Maximum price"),
] = None,
) -> list[dict]:
# Your logic here to search products
# Use query, min_price, and max_price parameters
return []
Here, query
, min_price
, and max_price
are all optional parameters. Users can search by keywords, price range, or a combination of both. If no parameters are provided, you might return a list of all products or a set of featured products.
3. Handling Dates and Time Ranges
For APIs dealing with time-series data or events, you might need to allow users to filter data by date or time ranges.
from litestar import get
from litestar.params import Parameter
from typing import Annotated, Optional
import datetime
@get(path="/events")
async def get_events(
start_date: Annotated[
Optional[datetime.date],
Parameter(title="Start Date", description="Start date for filtering events"),
] = None,
end_date: Annotated[
Optional[datetime.date],
Parameter(title="End Date", description="End date for filtering events"),
] = None,
) -> list[dict]:
# Your logic here to filter events by date range
# Use start_date and end_date parameters
return []
In this case, start_date
and end_date
are optional date parameters. Users can fetch events within a specific date range or retrieve all events if no dates are provided.
Common Mistakes to Avoid
While using Optional
and None
is the recommended approach, there are some common mistakes you should avoid when working with optional query parameters in Litestar.
1. Not Handling None
Values
It's crucial to handle None
values explicitly in your route handler. If you assume a parameter will always have a value, you might encounter unexpected errors.
from litestar import get
from litestar.params import Parameter
from typing import Annotated, Optional
@get(path="/items")
async def get_items(
category: Annotated[
Optional[str],
Parameter(title="Category", description="Filter items by category"),
] = None,
) -> list[dict]:
if category:
# Your logic to filter items by category
pass
else:
# Your logic to return all items
pass
return []
2. Using the Wrong Type Annotation
Make sure you use the correct type annotation for your parameter. If you expect an integer, use Optional[int]
; if you expect a string, use Optional[str]
, and so on.
3. Forgetting to Import Optional
Don't forget to import Optional
from the typing
module. If you miss this import, your code will raise a NameError
.
4. Overcomplicating Parameter Definitions
Keep your parameter definitions clean and straightforward. Avoid unnecessary complexity, especially when dealing with optional parameters. The Optional
and None
approach is usually the simplest and most effective.
Conclusion: Mastering Optional Query Parameters in Litestar
Alright, guys, we've covered a lot! We've explored a bug in Litestar where query parameters don't default to empty as expected, leading to validation errors. We've learned the importance of using Optional
and None
for optional parameters and how this approach ensures that your APIs behave predictably.
By understanding how Litestar handles parameters and following the best practices we've discussed, you can confidently define optional query parameters in your applications. You'll be able to create flexible, user-friendly APIs that handle a wide range of use cases, from filtering and pagination to searching and time-range filtering.
Remember, the key takeaways are:
- Use
Optional[YourType]
for optional parameters. - Set the default value to
None
. - Handle
None
values explicitly in your route handlers.
With these principles in mind, you'll be well-equipped to tackle any challenge involving query parameters in Litestar. Keep building awesome APIs!
If you have any questions or run into further issues, don't hesitate to ask. Happy coding!