lambda-preflight
Catch AWS Lambda IAM permission errors and event-shape bugs before you deploy.
Every "local lambda" tool runs your handler. lambda-preflight does something none of them do: it checks your handler's AWS calls against your execution role's real IAM policies and fails locally — so you find the missing s3:PutObject on your laptop, not in a CloudWatch log after a failed deploy.
No Docker. No SAM CLI. No cloud invocations, so no invocation charges.
pip install "lambda-preflight[aws]"
The headline: local IAM linting
Your Lambda runs as an execution role. When that role is missing a permission, you normally find out the slow way — deploy, invoke, read the AccessDenied in the logs, redeploy. lambda-preflight pulls your role's real policies once, caches them, and checks every AWS call your handler makes before anything leaves your machine.
from lambda_preflight import pull_role, PolicyEvaluator, intercept_boto3
from lambda_preflight import LambdaHarness, events
# Pull the execution role's real policies (managed + inline + boundary), cached after first call.
policies = pull_role("my-function-exec-role")
evaluator = PolicyEvaluator(policies, strict=True)
harness = LambdaHarness("app.handler", timeout=30)
# boto3 calls inside the handler are authorized against the real policy — locally.
with intercept_boto3(evaluator):
result = harness.invoke(events.sqs(body={"orderId": 123}))
# If app.handler calls s3:PutObject and the role can't → AccessDenied raised here,
# with the same reason AWS would give, before you ever deploy.
Three ways to load policies, so it works on a plane or in locked-down accounts:
from lambda_preflight import pull_role, load_role_policies, PolicySet
pull_role("my-exec-role") # 1. live pull — needs creds + IAM read, caches result
load_role_policies("my-exec-role") # 2. offline — reuse cache, no AWS call, no charge
PolicySet.from_policy_documents([{"Statement": [...]}]) # 3. BYO JSON — when you can't pull
It models the parts that actually decide real permissions: explicit-deny-wins, implicit deny, wildcard actions, resource matching, permission-boundary intersection, and common condition operators. What it can't faithfully evaluate (uncommon condition keys, SCPs, resource-based policies) it flags — in strict=True mode it raises CannotEvaluate rather than returning a false green. It tells you the edge of its own knowledge instead of guessing.
The foundation: an AWS-faithful local runtime
The linter needs something real to run against, so the runtime half is built to match AWS's actual invocation contract — not a stubbed approximation.
Schema-accurate event fixtures
The #1 cause of "worked locally, broke in AWS" is a test event whose shape is wrong. These builders emit payloads matching AWS's real schemas, so what you test is what you'll get. REST (v1) and HTTP (v2) API Gateway differ meaningfully — they're separate on purpose:
events.api_gateway_v1(method="GET", path="/health")
events.api_gateway_v2(method="POST", path="/orders", body={"id": 1})
events.sqs(records=[{"a": 1}, {"b": 2}]) # real batch shape, messageAttributes, md5
events.sns(message={"x": 1}, subject="hi")
events.s3(bucket="b", key="k.txt", event_name="ObjectCreated:Put")
events.eventbridge(detail={"k": "v"}, detail_type="my.type", source="svc")
events.dynamodb_stream(keys={"id": {"S": "1"}}, event_name="INSERT")
Faithful invocation semantics
result = harness.invoke(event)
result.ok # success vs. error
result.payload # what the handler returned
result.error # {errorType, errorMessage, stackTrace} — AWS's exact envelope
result.cold_start # models warm-container reuse across invokes
The context reproduces real fields (aws_request_id, invoked_function_arn, log names) and a live get_remaining_time_in_millis() countdown. Timeouts are enforced the way AWS enforces them. Lambda's environment variables are injected so handler code that reads them behaves the same.
pytest, because CI is the point
Fixtures register automatically — fast, cloud-free, zero-charge tests:
def test_orders(lambda_harness, lambda_events):
harness = lambda_harness("app.handler", timeout=5)
result = harness.invoke(lambda_events.sqs(body={"order": 42}))
assert result.ok
How this differs from other local-lambda tools
Tools like python-lambda-local and local-aws-lambda are runners — they execute your handler against an event dict you supply. That's the commodity half, and it's solved. lambda-preflight includes a competent runner too, but its reason to exist is what sits on top:
| Typical local-lambda tools | lambda-preflight | |
|---|---|---|
| Run a handler locally | ✅ | ✅ |
| Schema-accurate event fixtures | ✋ you write the dict | ✅ built-in, v1 ≠ v2 |
| Faithful context / timeout / cold-start / AWS-shape errors | partial | ✅ |
| IAM permission linting against your real role | ❌ | ✅ unique |
Honest scope
This kills the shape / contract / runtime-semantics / identity-permission class of "works locally, breaks in AWS" failures — which is most of the day-to-day pain. It does not, and a local tool cannot, reproduce cloud-only realities: cold-start latency, live concurrency, account quotas, SCPs, and resource-based policies (the last two are recorded in PolicySet.unscoped, never silently ignored). The IAM layer is a pre-flight linter, not a parity guarantee. For final confidence, run against real AWS credentials.
License
MIT
Release files for lambda-preflight 0.1.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| lambda_preflight-0.1.1.tar.gz | 28.9 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| lambda_preflight-0.1.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 54.7 kB
Release files / lambda_preflight-0.1.1.tar.gz
| Download URL | lambda_preflight-0.1.1.tar.gz |
|---|---|
| Size | 28.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
953f78ad37ce0bd797a1a7dda762e3dae7614316aa73a6941380ce8541262481
|
|
BLAKE2b-256 checksum How to use checksums |
73a031c629032b31063771f1108bea5163ef39b0df8b8ee0b894810cdb3c38ea
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.9.0
|
Release files / lambda_preflight-0.1.1-py3-none-any.whl
| Download URL | lambda_preflight-0.1.1-py3-none-any.whl |
|---|---|
| Size | 25.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
c93062b9caef774a48e8694c0bca47ea9196cca2a0ac896c42dd9016e204ead5
|
|
BLAKE2b-256 checksum How to use checksums |
8fc5c6c4c1dc35514a4b1b046ab3cf2a5a504b7b14061686451d6685361d83d6
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.9.0
|