Skip to content

fix(idempotency): fix str(None) in Redis persistence and extract duplicated null-check helper - tech debt 8090#8176

Open
hirenkumar-n-dholariya wants to merge 7 commits intoaws-powertools:developfrom
hirenkumar-n-dholariya:fix/idempotency-tech-debt-8090
Open

fix(idempotency): fix str(None) in Redis persistence and extract duplicated null-check helper - tech debt 8090#8176
hirenkumar-n-dholariya wants to merge 7 commits intoaws-powertools:developfrom
hirenkumar-n-dholariya:fix/idempotency-tech-debt-8090

Conversation

@hirenkumar-n-dholariya
Copy link
Copy Markdown
Contributor

@hirenkumar-n-dholariya hirenkumar-n-dholariya commented Apr 27, 2026

Issue number: fixes #8090

Summary

Fixes all 4 tech debt issues found during code review of the idempotency utility.

Changes

aws_lambda_powertools/utilities/idempotency/base.py

  • Fix falsy response handling: if response else Noneif response is not None else None
    so valid falsy return values (0, False, {}, [], "") are correctly serialized and stored instead of being silently discarded on replay
  • Fix inconsistent status constant: string literal "INPROGRESS"
    STATUS_CONSTANTS["INPROGRESS"] for consistency with the rest of the codebase

aws_lambda_powertools/utilities/idempotency/persistence/redis.py

  • Fix str(None) producing the string "None" in _item_to_data_record() when data_attr or validation_key_attr is missing from Redis

User experience

Before: Functions returning falsy values (0, False, {}) would have their response silently discarded and re-executed on replay instead of returning the cached value.

After: All non-None return values including falsy ones are correctly cached and returned on replay.

Tests

Added one regression test in tests/functional/idempotency/_redis/test_redis_layer.py next to existing test_item_to_datarecord_conversion, verifying missing Redis attributes return None instead of the string "None".

Acknowledgment


By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.

Disclaimer: We value your time and bandwidth. As such, any pull requests created on non-triaged issues might not be successful.

Copy link
Copy Markdown
Contributor

@leandrodamascena leandrodamascena left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @hirenkumar-n-dholariya, thanks for working on this and submitting the PR!

The Redis str(None) fix is good. Removing the str() wrapper is the right fix.

However, I have some concerns with the rest of the PR, and there are missing items from the issue.

Missing items from the issue

In your comment on #8090 you mentioned that items 1 and 2 "appear already resolved", but they are still present in the codebase:

Item 1 - Falsy response handling is still in base.py:299:

serialized_response: dict = self.output_serializer.to_dict(response) if response else None

This silently discards valid falsy return values like 0, False, "", [], {}. It should be if response is not None. This is a real bug that affects customers.

Item 2 - "INPROGRESS" string literal is still in base.py:170:

if is_replay and record is not None and record.status == "INPROGRESS":

Should use STATUS_CONSTANTS["INPROGRESS"] for consistency with the rest of the code.

Helper method

The helper _get_idempotency_key_or_return_none() doesn't actually reduce the duplication. After the refactor, each caller still does:

idempotency_key = self._get_idempotency_key_or_return_none(data=data)
if idempotency_key is None:
    return None

We moved one line into a wrapper but the if None: return is still in all 4 methods. The helper is a passthrough that adds indirection without simplifying anything. Let's drop this part and keep the original inline pattern, it's 3 lines and every Python developer reads it instantly.

Tests

The tests need a full rework. Please don't create a new test file for this.

We already have test_item_to_datarecord_conversion in tests/functional/idempotency/_redis/test_redis_layer.py (line 318) that tests exactly _item_to_data_record(). Add your test case there, next to the existing one. That's our pattern: tests go in the existing test files, close to related tests.

What's wrong with the current tests:

  • Don't test Python builtins. test_str_none_produces_wrong_string asserts that str(None) == "None" and test_redis_missing_data_attr_returns_none_not_string asserts that dict.get() returns None. These are testing the Python language, not our code.
  • Don't mock internal methods with MagicMock. We don't do that in idempotency tests. Look at the existing tests in _redis/test_redis_layer.py and _boto3/test_idempotency.py for the patterns we use. We test through the actual classes with fixtures, not by patching _get_hashed_idempotency_key with a MagicMock.
  • ConcretePersistenceLayer is defined 5 times instead of once.
  • 208 lines of tests for a 2-line fix is too much surface area. A single test in the existing file that calls _item_to_data_record() with missing attributes and asserts response_data is None (not "None") would cover the Redis fix properly.

Suggestion

Let's scope this PR to fix all 4 items from the issue:

  1. Fix if response else None to if response is not None (item 1 - bug fix)
  2. Fix "INPROGRESS" string literal to use STATUS_CONSTANTS (item 2 - consistency)
  3. Fix str(None) in Redis (item 3 - already done, good)
  4. Drop the helper method extraction (item 4 - the current approach doesn't improve things)

Add tests in the existing test files following our current patterns. Also pls fix the issues in SonarCloud.

Thanks again for contributing!

@powertools-for-aws-oss-automation powertools-for-aws-oss-automation Bot added size/L Denotes a PR that changes 100-499 lines, ignoring generated files. size/S Denotes a PR that changes 10-29 lines, ignoring generated files. and removed size/L Denotes a PR that changes 100-499 lines, ignoring generated files. size/S Denotes a PR that changes 10-29 lines, ignoring generated files. labels Apr 28, 2026
@hirenkumar-n-dholariya
Copy link
Copy Markdown
Contributor Author

hirenkumar-n-dholariya commented Apr 28, 2026

Hey @hirenkumar-n-dholariya, thanks for working on this and submitting the PR!

The Redis str(None) fix is good. Removing the str() wrapper is the right fix.

However, I have some concerns with the rest of the PR, and there are missing items from the issue.

Missing items from the issue

In your comment on #8090 you mentioned that items 1 and 2 "appear already resolved", but they are still present in the codebase:

Item 1 - Falsy response handling is still in base.py:299:

serialized_response: dict = self.output_serializer.to_dict(response) if response else None

This silently discards valid falsy return values like 0, False, "", [], {}. It should be if response is not None. This is a real bug that affects customers.

Item 2 - "INPROGRESS" string literal is still in base.py:170:

if is_replay and record is not None and record.status == "INPROGRESS":

Should use STATUS_CONSTANTS["INPROGRESS"] for consistency with the rest of the code.

Helper method

The helper _get_idempotency_key_or_return_none() doesn't actually reduce the duplication. After the refactor, each caller still does:

idempotency_key = self._get_idempotency_key_or_return_none(data=data)
if idempotency_key is None:
    return None

We moved one line into a wrapper but the if None: return is still in all 4 methods. The helper is a passthrough that adds indirection without simplifying anything. Let's drop this part and keep the original inline pattern, it's 3 lines and every Python developer reads it instantly.

Tests

The tests need a full rework. Please don't create a new test file for this.

We already have test_item_to_datarecord_conversion in tests/functional/idempotency/_redis/test_redis_layer.py (line 318) that tests exactly _item_to_data_record(). Add your test case there, next to the existing one. That's our pattern: tests go in the existing test files, close to related tests.

What's wrong with the current tests:

  • Don't test Python builtins. test_str_none_produces_wrong_string asserts that str(None) == "None" and test_redis_missing_data_attr_returns_none_not_string asserts that dict.get() returns None. These are testing the Python language, not our code.
  • Don't mock internal methods with MagicMock. We don't do that in idempotency tests. Look at the existing tests in _redis/test_redis_layer.py and _boto3/test_idempotency.py for the patterns we use. We test through the actual classes with fixtures, not by patching _get_hashed_idempotency_key with a MagicMock.
  • ConcretePersistenceLayer is defined 5 times instead of once.
  • 208 lines of tests for a 2-line fix is too much surface area. A single test in the existing file that calls _item_to_data_record() with missing attributes and asserts response_data is None (not "None") would cover the Redis fix properly.

Suggestion

Let's scope this PR to fix all 4 items from the issue:

  1. Fix if response else None to if response is not None (item 1 - bug fix)
  2. Fix "INPROGRESS" string literal to use STATUS_CONSTANTS (item 2 - consistency)
  3. Fix str(None) in Redis (item 3 - already done, good)
  4. Drop the helper method extraction (item 4 - the current approach doesn't improve things)

Add tests in the existing test files following our current patterns. Also pls fix the issues in SonarCloud.

Thanks again for contributing!

Hey @leandrodamascena,
Thank you for the thorough and detailed review - really appreciate the clear guidance!

I have addressed all the feedback:

  1. Fixed falsy response handling - I changed if response else None to if response is not None else None in idempotency/base.py so valid falsy return values (0, False, {}, [], "") are correctly cached and returned on replay

  2. Fixed STATUS_CONSTANTS consistency - I replaced string literal "INPROGRESS" with STATUS_CONSTANTS["INPROGRESS"] in_process_idempotency()`

  3. Reverted helper method - restored the original inline 3-line null-check pattern across all 4 methods in persistence/base.py. You are right that the helper added indirection without real simplification.

  4. Tests reworked - removed the standalone test file and added a single focused test directly in _redis/test_redis_layer.py next to test_item_to_datarecord_conversion, following the existing fixture-based patterns

  5. Fixed PR description - added the Acknowledgment section as required by the template

Thanks again for taking the time to explain the patterns and reasoning behind each point. It really helps understand the project's conventions better. Please let me know if anything else needs adjusting.

…istence

Replace str(item.get(...)) with item.get(...) to avoid storing the
string "None" when a value is missing from Redis hash map.

When data_attr or validation_key_attr is missing from Redis,
item.get() returns None. Wrapping with str() converts it to the
string "None" which is incorrect. Now correctly returns None.

Part of aws-powertools#8090

Signed-off-by: hirenkumar-n-dholariya <hirenkumarnd@gmail.com>
…into helper method

Replace 4 identical null-check blocks across save_success, save_inprogress, delete_record, and get_record with a single helper method _get_idempotency_key_or_return_none() to reduce code duplication.

The helper encapsulates the pattern of calling _get_hashed_idempotency_key() and returning None early if the key is None, keeping each method cleaner and easier to read.

Part of aws-powertools#8090

Signed-off-by: hirenkumar-n-dholariya <hirenkumarnd@gmail.com>
…wertools#8090

Fix 1 - str(None) in Redis _item_to_data_record:
- Missing data_attr returns None not string "None"
- Existing data_attr returns value correctly
- Demonstrates old bug vs new correct behavior

Fix 2 - _get_idempotency_key_or_return_none helper:
- Returns None when key is None
- Returns key string when key exists
- Correctly used in save_success, save_inprogress,
  delete_record, and get_record (all return None early)

Part of aws-powertools#8090

Signed-off-by: hirenkumar-n-dholariya <hirenkumarnd@gmail.com>
… constant

Fix 1 - Falsy response handling in _get_function_response(): Replace `if response else None` with `if response is not None else None`. So valid falsy return values (0, False, {}, [], "") are correctly   serialized and stored instead of being silently discarded.

Fix 2 - Inconsistent status constant in _process_idempotency():
Replace string literal "INPROGRESS" with STATUS_CONSTANTS["INPROGRESS"] for consistency with the rest of the codebase.

Part of aws-powertools#8090

Signed-off-by: hirenkumar-n-dholariya <hirenkumarnd@gmail.com>
… null-check pattern

Per Leandro Damascena's feedback: _get_idempotency_key_or_return_none() helper added
indirection without reducing duplication since the if None: return None check remained in all 4 callers anyway.

Restored original inline 3-line pattern in save_success, save_inprogress, delete_record, and get_record which is clearer and instantly readable.

Part of aws-powertools#8090

Signed-off-by: hirenkumar-n-dholariya <hirenkumarnd@gmail.com>
Tests should be added to existing test files following established patterns, not in new standalone files. The Redis fix will be tested in
_redis/test_redis_layer.py next to the existing test_item_to_datarecord_conversion.

Part of aws-powertools#8090

Signed-off-by: hirenkumar-n-dholariya <hirenkumarnd@gmail.com>
…data_record

Added single test next to existing test_item_to_datarecord_conversion to verify missing Redis attributes return None instead of string "None".

Follows existing test patterns using fixtures instead of MagicMock.

Part of aws-powertools#8090

Signed-off-by: hirenkumar-n-dholariya <hirenkumarnd@gmail.com>
@hirenkumar-n-dholariya hirenkumar-n-dholariya force-pushed the fix/idempotency-tech-debt-8090 branch from e78eeb2 to 01f565a Compare April 28, 2026 11:07
@powertools-for-aws-oss-automation powertools-for-aws-oss-automation Bot added size/S Denotes a PR that changes 10-29 lines, ignoring generated files. and removed size/S Denotes a PR that changes 10-29 lines, ignoring generated files. labels Apr 28, 2026
@sonarqubecloud
Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/S Denotes a PR that changes 10-29 lines, ignoring generated files. tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Tech debt: Fix falsy response handling, inconsistencies, and duplicated code in idempotency utility

2 participants