Static typing: allow to pass dict as argument to exception classes
laurens-teirlynck opened this issue ยท 14 comments
Static type checker used
mypy (project's standard)
AWS Lambda function runtime
3.13
Powertools for AWS Lambda (Python) version
latest
Static type checker info
When trying to call this endpoint, a application/json response is returned, but the typing in the exception classes from event_handler/exceptions.py only allow str.
http -v :3000/hello/bob
GET /hello/bob HTTP/1.1
Accept: */*
Accept-Encoding: gzip, deflate
Connection: keep-alive
Host: localhost:3000
User-Agent: HTTPie/3.2.4
HTTP/1.1 404 NOT FOUND
Connection: close
Content-Length: 82
Content-Type: application/json
Date: Fri, 29 Aug 2025 09:31:53 GMT
Server: Werkzeug/3.1.3 Python/3.13.7
{
"message": {
"msg": "no person named bob",
"type": "name.not_found"
},
"statusCode": 404
}
Code snippet
from aws_lambda_powertools.event_handler import APIGatewayRestResolver
from aws_lambda_powertools.event_handler.exceptions import NotFoundError
app = APIGatewayRestResolver()
@app.get("/hello/<name>")
def hello_name(name: str):
raise NotFoundError(
{
"type": "name.not_found",
"msg": f"no person named {name}",
}
)
def lambda_handler(event, context):
return app.resolve(event, context)Possible Solution
A simple fix would be to change
class ServiceError(Exception):
"""Powertools class HTTP Service Error"""
def __init__(self, status_code: int, msg: str):with
class ServiceError(Exception):
"""Powertools class HTTP Service Error"""
def __init__(self, status_code: int, msg: str | dict):But I'm not familiar enough with the codebase to know if this can cause other issues.
Thanks for opening your first issue here! We'll come back to you as soon as we can.
In the meantime, check out the #python channel on our Powertools for AWS Lambda Discord: Invite link
Hello @laurens-teirlynck ! Thank you for opening this issue. I was able to reproduce it, and the typing annotations don't match the runtime behavior.
I'll add it to the backlog and let you know if we need more information!
Hey @laurens-teirlynck feel free to submit a PR with this change. I think it's ok we change this function signature.
In my own code, I added a # type: ignore for now, but realised while writing a test that the response body contains the status code, and the error from the function got wrapped inside a message object:
{
"message": {
"msg": "no person named bob",
"type": "name.not_found"
},
"statusCode": 404
}My first intuition would be that this would have returned the following as body
{
"msg": "no person named bob",
"type": "name.not_found"
}In my own code, I added a
# type: ignorefor now, but realised while writing a test that the response body contains the status code, and the error from the function got wrapped inside amessageobject:{
"message": {
"msg": "no person named bob",
"type": "name.not_found"
},
"statusCode": 404
}
Yes, this is true.
My first intuition would be that this would have returned the following as body
{
"msg": "no person named bob",
"type": "name.not_found"
}
But I'm a little bit confused now. It still will trigger mypy and pyright errors for those that want to return a JSON like this:
def hello_name(name: str):
raise NotFoundError(
{
"type": "name.not_found",
"msg": f"no person named {name}",
}
)Are you happy your current solution? I'll leave this issue for a while if someone wants to pick up this.
For those that are reading this issue, here is what needs to be changed:
In aws_lambda_powertools/event_handler/exceptions.py, update all exception class constructors from:
def __init__(self, msg: str):
To:
def __init__(self, msg: str | dict):
Files to modify:
aws_lambda_powertools/event_handler/exceptions.py
Classes that need updating:
ServiceError
BadRequestError
UnauthorizedError
ForbiddenError
NotFoundError
RequestTimeoutError
RequestEntityTooLargeError
InternalServerError
ServiceUnavailableError
Steps:
- Change the type annotation from
strtostr | dictin each constructor - Update the docstrings to mention that msg can be either a string or dictionary
- Add at least one test to verify dict messages work correctly
Good first issue for someone wanting to contribute! ๐โโ๏ธ
Hi, I would like to work on this issue.
Could you please assign it to me?
Thanks.
Hi, I would like to work on this issue.
Could you please assign it to me?
Thanks.
Hey @shrivarshapoojari sure thing! I'm assigning to you. Thanks
Hi,
I have added the type annotations and docstrings to mentioned exceptions.py
I'm having trouble locating existing test files related to these exception module.
Could you please help me understand where tests for the event handler exceptions should be added according to the repository's standard practices?
I want to make sure I follow the existing patterns and conventions.
I've looked in tests/unit/event_handler/ but couldn't find any specific test files for the exceptions module.
Should I:
- Create a new test file like
test_exceptions.pyin the event handler test directory? - Place the tests somewhere else entirely?
Thanks
In my own code, I added a
# type: ignorefor now, but realised while writing a test that the response body contains the status code, and the error from the function got wrapped inside amessageobject:
{
"message": {
"msg": "no person named bob",
"type": "name.not_found"
},
"statusCode": 404
}Yes, this is true.
So it is expected that the statusCode also gets returned as part of the response body?
My first intuition would be that this would have returned the following as body
{
"msg": "no person named bob",
"type": "name.not_found"
}But I'm a little bit confused now. It still will trigger mypy and pyright errors for those that want to return a JSON like this:
Maybe to try and make it a bit clearer, I've updated my example to something that looks more like what I'm trying to do:
from pydantic import BaseModel
from aws_lambda_powertools.event_handler import APIGatewayRestResolver
from aws_lambda_powertools.event_handler.exceptions import NotFoundError
app = APIGatewayRestResolver(enable_validation=True)
class HelloResponse(BaseModel):
hello: str
@app.get("/hello/<name>")
def hello_name(name: str) -> HelloResponse:
if name == "bob":
raise NotFoundError(
{
"type": "name.not_found",
"msg": f"no person named {name}",
}
)
return HelloResponse(hello=name)
def lambda_handler(event, context):
return app.resolve(event, context)When calling this with
http -v :3000/hello/bob
GET /hello/bob HTTP/1.1
Accept: */*
Accept-Encoding: gzip, deflate
Connection: keep-alive
Host: localhost:3000
User-Agent: HTTPie/3.2.4
HTTP/1.1 404 NOT FOUND <---- normal HTTP response status code
Connection: close
Content-Length: 82
Content-Type: application/json
Date: Fri, 05 Sep 2025 12:09:59 GMT
Server: Werkzeug/3.1.3 Python/3.13.7
{
"message": {
"msg": "no person named bob",
"type": "name.not_found"
},
"statusCode": 404 <--- Second HTTP response status code
}I would not expect the second HTTP Response status code to be in here.
def hello_name(name: str):
raise NotFoundError(
{
"type": "name.not_found",
"msg": f"no person named {name}",
}
)Are you happy your current solution? I'll leave this issue for a while if someone wants to pick up this.
So I would update more than just the type hint so that the status code isn't part of the response body.
But I'm a little bit confused now. It still will trigger mypy and pyright errors for those that want to return a JSON like this:
I don't get a mypy error with the code from this snippet outside that NotFoundError expects a str as message at this moment in time.
Maybe to try and make it a bit clearer, I've updated my example to something that looks more like what I'm trying to do:
from pydantic import BaseModel
from aws_lambda_powertools.event_handler import APIGatewayRestResolver
from aws_lambda_powertools.event_handler.exceptions import NotFoundErrorapp = APIGatewayRestResolver(enable_validation=True)
class HelloResponse(BaseModel):
hello: str@app.get("/hello/")
def hello_name(name: str) -> HelloResponse:
if name == "bob":
raise NotFoundError(
{
"type": "name.not_found",
"msg": f"no person named {name}",
}
)return HelloResponse(hello=name)def lambda_handler(event, context):
return app.resolve(event, context)
When calling this withhttp -v :3000/hello/bob
GET /hello/bob HTTP/1.1
Accept: /
Accept-Encoding: gzip, deflate
Connection: keep-alive
Host: localhost:3000
User-Agent: HTTPie/3.2.4HTTP/1.1 404 NOT FOUND <---- normal HTTP response status code
Connection: close
Content-Length: 82
Content-Type: application/json
Date: Fri, 05 Sep 2025 12:09:59 GMT
Server: Werkzeug/3.1.3 Python/3.13.7{
"message": {
"msg": "no person named bob",
"type": "name.not_found"
},
"statusCode": 404 <--- Second HTTP response status code
}
I would not expect the second HTTP Response status code to be in here.
Thanks for the explanation. Now I understand the issue you're facing, and to be honest, we can't change this behavior right now. We can only update the message type annotation to accept str and dict for now.
All HTTP exceptions inherit from ServiceError, and we serialize these exceptions here. For some reason that I need to investigate further, it's serializing {"statusCode":404,"message":{"type":"name.not_found","msg":"no person named bob"}} as part of the body and not as the object itself to be returned.
What is happening is this - see the field body:
{
"statusCode":"<HTTPStatus.NOT_FOUND":404>,
"body":"{\"statusCode\":404,\"message\":{\"type\":\"name.not_found\",\"msg\":\"no person named bob\"}}",
"isBase64Encoded":false,
"multiValueHeaders":"defaultdict(<class""list"">",
{
"Content-Type":[
"application/json"
]
}")"
}I need to investigate further, but as I said, I can't change this right now because it could break clients that rely on this current behavior. But as alternative you can use the Response object that you'll have the result you want: https://docs.powertools.aws.dev/lambda/python/latest/core/event_handler/api_gateway/#fine-grained-responses
For now I'm adding this issue to the discussion where we are discussing ideas and breaking changes for Powertools for AWS Python v4: #5948 (comment)
Thanks a lot for reporting this.
Hi,
I have added the type annotations and docstrings to mentioned exceptions.py
I'm having trouble locating existing test files related to these exception module.
Could you please help me understand where tests for the event handler exceptions should be added according to the repository's standard practices? I want to make sure I follow the existing patterns and conventions.
I've looked in
tests/unit/event_handler/but couldn't find any specific test files for the exceptions module. Should I:
- Create a new test file like
test_exceptions.pyin the event handler test directory?- Place the tests somewhere else entirely?
Thanks
Hey @shrivarshapoojari I was reading our tests again and we already have a test for this, so, doesn't need to add a test. You can open the PR ๐
Hey @shrivarshapoojari I was reading our tests again and we already have a test for this, so, doesn't need to add a test. You can open the PR ๐
Hi @leandrodamascena
Submitted the PR.
Please do review and let me know if any changes required.
Thanks
Warning
This issue is now closed. Please be mindful that future comments are hard for our team to see.
If you need more assistance, please either reopen the issue, or open a new issue referencing this one.
If you wish to keep having a conversation with other community members under this issue feel free to do so.