Summary
A boolean flag satisfies an Integer request. Evaluating a boolean flag through get_integer_details returns True with reason STATIC and no error code, where the specification requires the code default and TYPE_MISMATCH.
The application sees a plausible value — True behaves as 1 in arithmetic — and no indication that anything went wrong. That is the worst failure mode a feature flag has.
Cause
_typecheck_flag_value in openfeature/client.py:
type_map: TypeMap = {
FlagType.BOOLEAN: bool,
FlagType.STRING: str,
FlagType.OBJECT: (dict, list),
FlagType.FLOAT: float,
FlagType.INTEGER: int,
}
py_type = type_map.get(flag_type)
...
if not isinstance(value, py_type):
return TypeMismatchError(f"Expected type {py_type} but got {type(value)}")
bool is a subclass of int in Python, so isinstance(True, int) is True and the check passes:
>>> isinstance(True, int)
True
The reverse direction is fine — isinstance(1, float) is False, so an integer requested as a float is correctly a mismatch — which is why this is easy to miss.
Reproducing
from openfeature import api
from openfeature.provider.in_memory_provider import InMemoryFlag, InMemoryProvider
api.set_provider(InMemoryProvider({
"boolean-flag": InMemoryFlag(default_variant="on", variants={"on": True, "off": False}),
}))
details = api.get_client().get_integer_details("boolean-flag", 1)
print(details.value, details.reason, details.error_code)
# True Reason.STATIC None
# expected: 1 Reason.ERROR ErrorCode.TYPE_MISMATCH
Suggested fix
Reject bool explicitly when an integer or float was requested:
if flag_type in (FlagType.INTEGER, FlagType.FLOAT) and isinstance(value, bool):
return TypeMismatchError(f"Expected type {py_type} but got {type(value)}")
Worth checking the same place for FlagType.OBJECT: (dict, list) accepts a list, which is correct per the spec's structure type, but is worth a test either way.
How it surfaced
Building the Python implementation of the cross-language provider conformance suite (spec#417). The suite runs an identical type-mismatch matrix against every provider in every language; this row passes everywhere else and fails only in Python, because only Python has bool as a subclass of int.
That is a reasonable advertisement for having more than one language implementation: a Java or Go suite could never have caught this.
The suite currently marks the case xfail(strict=True) with a pointer to this issue, so it stays visible and un-hides itself automatically once this is fixed.
Summary
A boolean flag satisfies an Integer request. Evaluating a boolean flag through
get_integer_detailsreturnsTruewith reasonSTATICand no error code, where the specification requires the code default andTYPE_MISMATCH.The application sees a plausible value —
Truebehaves as1in arithmetic — and no indication that anything went wrong. That is the worst failure mode a feature flag has.Cause
_typecheck_flag_valueinopenfeature/client.py:boolis a subclass ofintin Python, soisinstance(True, int)isTrueand the check passes:The reverse direction is fine —
isinstance(1, float)isFalse, so an integer requested as a float is correctly a mismatch — which is why this is easy to miss.Reproducing
Suggested fix
Reject
boolexplicitly when an integer or float was requested:Worth checking the same place for
FlagType.OBJECT:(dict, list)accepts alist, which is correct per the spec's structure type, but is worth a test either way.How it surfaced
Building the Python implementation of the cross-language provider conformance suite (spec#417). The suite runs an identical type-mismatch matrix against every provider in every language; this row passes everywhere else and fails only in Python, because only Python has
boolas a subclass ofint.That is a reasonable advertisement for having more than one language implementation: a Java or Go suite could never have caught this.
The suite currently marks the case
xfail(strict=True)with a pointer to this issue, so it stays visible and un-hides itself automatically once this is fixed.