Skip to content

Commit c3cb6ef

Browse files
Fix: Always evaluate holdouts for every feature (align with Swift SDK)
This commit fixes a critical architectural difference where Python conditionally entered the holdout evaluation path, while Swift always evaluates holdouts for every feature. ## Problem Python had two different code paths: 1. WITH holdouts: get_variation_for_feature → get_decision_for_flag 2. WITHOUT holdouts: get_variation_for_feature → get_variations_for_feature_list This created inconsistency and potential bugs. ## Solution Now Python ALWAYS goes through get_decision_for_flag: - get_variation_for_feature → get_decision_for_flag (always) - get_decision_for_flag checks holdouts → experiments → rollouts ## Changes optimizely/decision_service.py: - Removed conditional branching in get_variation_for_feature() - Always call get_decision_for_flag() regardless of holdout existence - Updated docstring to reflect Swift SDK alignment ## Alignment with Swift SDK Swift flow: getVariationForFeature → getVariationForFeatureList → getDecisionForFlag Python flow (now): get_variation_for_feature → get_decision_for_flag Both now: - Always evaluate holdouts first (even if empty list) - Use single unified code path - No conditional branching ## Testing ✓ Features without holdouts work correctly ✓ Features with holdouts work correctly ✓ Features with experiments work correctly ✓ Unified code path verified 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent 2650196 commit c3cb6ef

File tree

1 file changed

+8
-7
lines changed

1 file changed

+8
-7
lines changed

optimizely/decision_service.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -659,6 +659,9 @@ def get_variation_for_feature(
659659
) -> DecisionResult:
660660
""" Returns the experiment/variation the user is bucketed in for the given feature.
661661
662+
Aligned with Swift SDK which always evaluates holdouts for every feature.
663+
Swift: getVariationForFeature → getVariationForFeatureList → getDecisionForFlag (always)
664+
662665
Args:
663666
project_config: Instance of ProjectConfig.
664667
feature: Feature for which we are determining if it is enabled or not for the given user.
@@ -671,13 +674,11 @@ def get_variation_for_feature(
671674
- 'error': Boolean indicating if an error occurred during the decision process.
672675
- 'reasons': List of log messages representing decision making for the feature.
673676
"""
674-
holdouts = project_config.get_holdouts_for_flag(feature.key)
675-
676-
if holdouts:
677-
# Has holdouts - use get_decision_for_flag which checks holdouts first
678-
return self.get_decision_for_flag(feature, user_context, project_config, options)
679-
else:
680-
return self.get_variations_for_feature_list(project_config, [feature], user_context, options)[0]
677+
# CRITICAL FIX: Always call get_decision_for_flag (matching Swift SDK behavior)
678+
# Swift always goes through getDecisionForFlag which checks holdouts first,
679+
# then experiments, then rollouts - regardless of whether holdouts exist.
680+
# Previously Python had conditional logic that created two different code paths.
681+
return self.get_decision_for_flag(feature, user_context, project_config, options)
681682

682683
def get_decision_for_flag(
683684
self,

0 commit comments

Comments
 (0)