[{"data":1,"prerenderedAt":930},["ShallowReactive",2],{"page-\u002Fproperty-based-fuzz-testing-strategies\u002Fadvanced-property-based-testing\u002Ftesting-numeric-code-with-floats-and-nan-edge-cases\u002F":3},{"id":4,"title":5,"body":6,"description":896,"extension":897,"meta":898,"navigation":105,"path":926,"seo":927,"stem":928,"__hash__":929},"content\u002Fproperty-based-fuzz-testing-strategies\u002Fadvanced-property-based-testing\u002Ftesting-numeric-code-with-floats-and-nan-edge-cases\u002Findex.md","Testing Numeric Code with Floats and NaN Edge Cases",{"type":7,"value":8,"toc":885},"minimark",[9,40,43,48,73,77,143,266,384,388,401,425,429,440,447,462,530,534,537,552,562,568,600,604,607,627,645,656,663,736,740,810,814,823,833,843,847,876,881],[10,11,12,13,17,18,21,22,21,25,21,28,31,32,35,36,39],"p",{},"Numerical code is where property-based testing earns its keep fastest, and also where it produces the most confusing first failures. Hypothesis's ",[14,15,16],"code",{},"st.floats()"," generates exactly the values that numerical code tends to forget: ",[14,19,20],{},"nan",", ",[14,23,24],{},"inf",[14,26,27],{},"-inf",[14,29,30],{},"-0.0",", the largest finite double, the smallest subnormal, and numbers that differ by one unit in the last place. A mean function tested on ",[14,33,34],{},"[1.0, 2.0, 3.0]"," passes; the same function under Hypothesis meets ",[14,37,38],{},"[1e308, 1e308]",", overflows to infinity, and fails.",[10,41,42],{},"Those failures are almost all useful, but they need interpretation. Some expose genuine bugs — overflow in an intermediate sum, cancellation that destroys precision, a comparison that silently lets NaN through. Others expose assertions that are wrong for floating-point: exact equality where rounding makes it impossible, or relative tolerance where the true answer is zero. Working through them turns a vague sense that \"floats are tricky\" into an explicit contract for which values your code supports and how accurate it promises to be.",[44,45,47],"h2",{"id":46},"prerequisites","Prerequisites",[49,50,51,65],"ul",{},[52,53,54,21,57,60,61,64],"li",{},[14,55,56],{},"hypothesis >= 6.100",[14,58,59],{},"pytest >= 8.0",", optionally ",[14,62,63],{},"numpy >= 1.26",".",[52,66,67,68,64],{},"Background from ",[69,70,72],"a",{"href":71},"\u002Fproperty-based-fuzz-testing-strategies\u002Fadvanced-property-based-testing\u002F","Advanced property-based testing",[44,74,76],{"id":75},"solution","Solution",[78,79,84],"pre",{"className":80,"code":81,"language":82,"meta":83,"style":83},"language-python shiki shiki-themes github-light github-dark","# stats.py\nimport math\n\ndef mean(xs: list[float]) -> float:\n    if not xs:\n        raise ValueError(\"mean of empty sequence\")\n    if any(math.isnan(x) for x in xs):\n        raise ValueError(\"NaN in input\")\n    return math.fsum(xs) \u002F len(xs)            # fsum avoids accumulated rounding\n","python","",[14,85,86,94,100,107,113,119,125,131,137],{"__ignoreMap":83},[87,88,91],"span",{"class":89,"line":90},"line",1,[87,92,93],{},"# stats.py\n",[87,95,97],{"class":89,"line":96},2,[87,98,99],{},"import math\n",[87,101,103],{"class":89,"line":102},3,[87,104,106],{"emptyLinePlaceholder":105},true,"\n",[87,108,110],{"class":89,"line":109},4,[87,111,112],{},"def mean(xs: list[float]) -> float:\n",[87,114,116],{"class":89,"line":115},5,[87,117,118],{},"    if not xs:\n",[87,120,122],{"class":89,"line":121},6,[87,123,124],{},"        raise ValueError(\"mean of empty sequence\")\n",[87,126,128],{"class":89,"line":127},7,[87,129,130],{},"    if any(math.isnan(x) for x in xs):\n",[87,132,134],{"class":89,"line":133},8,[87,135,136],{},"        raise ValueError(\"NaN in input\")\n",[87,138,140],{"class":89,"line":139},9,[87,141,142],{},"    return math.fsum(xs) \u002F len(xs)            # fsum avoids accumulated rounding\n",[78,144,146],{"className":80,"code":145,"language":82,"meta":83,"style":83},"# test_stats.py\nimport math\nimport pytest\nfrom hypothesis import given, strategies as st\nfrom stats import mean\n\nfinite = st.floats(allow_nan=False, allow_infinity=False, width=64)\nmoderate = st.floats(-1e150, 1e150, allow_nan=False)\n\n@given(st.lists(moderate, min_size=1))\ndef test_mean_between_min_and_max(xs):\n    m = mean(xs)\n    assert min(xs) \u003C= m \u003C= max(xs)\n\n@given(st.lists(finite, min_size=1), st.just(float(\"nan\")))\ndef test_nan_is_rejected(xs, nan):\n    with pytest.raises(ValueError):\n        mean(xs + [nan])\n\n@given(moderate)\ndef test_mean_of_constant_list(x):\n    assert mean([x] * 7) == pytest.approx(x, rel=1e-12, abs=1e-300)\n",[14,147,148,153,157,162,167,172,176,181,186,190,196,202,208,214,219,225,231,237,243,248,254,260],{"__ignoreMap":83},[87,149,150],{"class":89,"line":90},[87,151,152],{},"# test_stats.py\n",[87,154,155],{"class":89,"line":96},[87,156,99],{},[87,158,159],{"class":89,"line":102},[87,160,161],{},"import pytest\n",[87,163,164],{"class":89,"line":109},[87,165,166],{},"from hypothesis import given, strategies as st\n",[87,168,169],{"class":89,"line":115},[87,170,171],{},"from stats import mean\n",[87,173,174],{"class":89,"line":121},[87,175,106],{"emptyLinePlaceholder":105},[87,177,178],{"class":89,"line":127},[87,179,180],{},"finite = st.floats(allow_nan=False, allow_infinity=False, width=64)\n",[87,182,183],{"class":89,"line":133},[87,184,185],{},"moderate = st.floats(-1e150, 1e150, allow_nan=False)\n",[87,187,188],{"class":89,"line":139},[87,189,106],{"emptyLinePlaceholder":105},[87,191,193],{"class":89,"line":192},10,[87,194,195],{},"@given(st.lists(moderate, min_size=1))\n",[87,197,199],{"class":89,"line":198},11,[87,200,201],{},"def test_mean_between_min_and_max(xs):\n",[87,203,205],{"class":89,"line":204},12,[87,206,207],{},"    m = mean(xs)\n",[87,209,211],{"class":89,"line":210},13,[87,212,213],{},"    assert min(xs) \u003C= m \u003C= max(xs)\n",[87,215,217],{"class":89,"line":216},14,[87,218,106],{"emptyLinePlaceholder":105},[87,220,222],{"class":89,"line":221},15,[87,223,224],{},"@given(st.lists(finite, min_size=1), st.just(float(\"nan\")))\n",[87,226,228],{"class":89,"line":227},16,[87,229,230],{},"def test_nan_is_rejected(xs, nan):\n",[87,232,234],{"class":89,"line":233},17,[87,235,236],{},"    with pytest.raises(ValueError):\n",[87,238,240],{"class":89,"line":239},18,[87,241,242],{},"        mean(xs + [nan])\n",[87,244,246],{"class":89,"line":245},19,[87,247,106],{"emptyLinePlaceholder":105},[87,249,251],{"class":89,"line":250},20,[87,252,253],{},"@given(moderate)\n",[87,255,257],{"class":89,"line":256},21,[87,258,259],{},"def test_mean_of_constant_list(x):\n",[87,261,263],{"class":89,"line":262},22,[87,264,265],{},"    assert mean([x] * 7) == pytest.approx(x, rel=1e-12, abs=1e-300)\n",[267,268,271,380],"figure",{"className":269},[270],"diagram",[272,273,280,281,280,285,280,289,280,297,280,307,280,313,280,320,280,325,280,331,280,337,280,346,280,350,280,353,280,356,280,359,280,361,280,364,280,371,280,376],"svg",{"viewBox":274,"role":275,"ariaLabelledBy":276,"xmlns":279},"0 0 800 246","img",[277,278],"fl-t","fl-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","\n  ",[282,283,284],"title",{"id":277},"The special values st.floats generates",[286,287,288],"desc",{"id":278},"A number line of double-precision floats marks the special values Hypothesis targets: negative infinity, the most negative finite value, negative and positive zero, subnormals near zero, the largest finite value, positive infinity, and NaN shown separately because it has no place on the line.",[290,291],"rect",{"x":292,"y":292,"width":293,"height":294,"rx":295,"fill":296},"0","800","246","14","#fffdf8",[298,299,306],"text",{"x":300,"y":301,"textAnchor":302,"fontSize":303,"fontWeight":304,"fill":305},"400","28","middle","15.5","700","#3d405b","Where floating-point code breaks",[89,308],{"x1":309,"y1":310,"x2":311,"y2":310,"stroke":305,"strokeWidth":312},"60","120","740","1.8",[314,315],"circle",{"cx":309,"cy":310,"r":316,"fill":317,"stroke":318,"strokeWidth":319},"7","#fbe9e3","#e07a5f","2",[298,321,27],{"x":309,"y":322,"textAnchor":302,"fontSize":323,"fill":324},"98","11","#8f3d22",[314,326],{"cx":327,"cy":310,"r":328,"fill":329,"stroke":330,"strokeWidth":319},"130","6","#f7f0da","#f2cc8f",[298,332,336],{"x":327,"y":333,"textAnchor":302,"fontSize":334,"fill":335},"150","10.5","#8a5a00","-1.79e308",[290,338],{"x":339,"y":340,"width":309,"height":341,"rx":342,"fill":343,"stroke":344,"strokeWidth":345},"370","110","20","4","#e6f0ea","#81b29a","1.6",[298,347,349],{"x":300,"y":322,"textAnchor":302,"fontSize":323,"fill":348},"#2a5f49","subnormals",[298,351,352],{"x":300,"y":333,"textAnchor":302,"fontSize":334,"fill":305},"-0.0 · 0.0",[314,354],{"cx":355,"cy":310,"r":328,"fill":329,"stroke":330,"strokeWidth":319},"670",[298,357,358],{"x":355,"y":333,"textAnchor":302,"fontSize":334,"fill":335},"1.79e308",[314,360],{"cx":311,"cy":310,"r":316,"fill":317,"stroke":318,"strokeWidth":319},[298,362,363],{"x":311,"y":322,"textAnchor":302,"fontSize":323,"fill":324},"+inf",[290,365],{"x":366,"y":367,"width":368,"height":369,"rx":370,"fill":305},"330","182","140","42","10",[298,372,375],{"x":300,"y":373,"textAnchor":302,"fontSize":374,"fontWeight":304,"fill":296},"200","12","NaN",[298,377,379],{"x":300,"y":378,"textAnchor":302,"fontSize":370,"fill":296},"216","off the line, != itself",[381,382,383],"figcaption",{},"Every marked point is generated deliberately; code that handles the middle of the line correctly still has to decide what happens at each of them.",[44,385,387],{"id":386},"why-this-works","Why this works",[10,389,390,392,393,396,397,400],{},[14,391,16],{}," is not a uniform sampler. It deliberately mixes special values, boundary values of any bounds you give, small integers expressed as floats, and values that are adjacent in the float representation. Shrinking then moves towards simple floats — integers, then short decimals — so a failure that first appears at ",[14,394,395],{},"1.2345e-310"," often shrinks to something like ",[14,398,399],{},"5e-324",", the smallest subnormal, which names the problem immediately.",[10,402,403,404,407,408,411,412,415,416,419,420,424],{},"The first property, that the mean lies between the minimum and maximum, looks mathematically trivial, and naive implementations still fail it. ",[14,405,406],{},"sum(xs) \u002F len(xs)"," overflows to infinity for two values near ",[14,409,410],{},"1e308",", and accumulated rounding error can push the result just outside the range for long lists of values with mixed magnitudes. ",[14,413,414],{},"math.fsum"," computes the exact rounded sum and fixes the rounding case; bounding the strategy to ",[14,417,418],{},"±1e150"," keeps the overflow case out of scope. That bound is a ",[421,422,423],"em",{},"decision",": the function now documents, via its test, that it supports values up to that magnitude.",[44,426,428],{"id":427},"choosing-tolerances-deliberately","Choosing tolerances deliberately",[10,430,431,432,435,436,439],{},"Exact equality on floats is right in fewer places than people think, and ",[14,433,434],{},"pytest.approx"," or ",[14,437,438],{},"math.isclose"," with default tolerances is right in fewer places still. Tolerances should come from the computation.",[10,441,442,443,446],{},"Relative tolerance expresses \"accurate to about this many significant digits\". A double carries roughly 15–16 significant decimal digits; each arithmetic operation can lose up to half a unit in the last place, and some operations — subtracting nearly equal numbers — lose far more. For a short computation, ",[14,444,445],{},"rel_tol=1e-12"," is a reasonable start. For iterative algorithms or matrix operations, the achievable accuracy depends on the condition number of the problem, and a looser tolerance with a comment explaining why is better than a tight one that flakes.",[10,448,449,450,453,454,457,458,461],{},"Absolute tolerance handles results near zero. When the true answer is ",[14,451,452],{},"0.0",", any computed ",[14,455,456],{},"1e-17"," has infinite relative error, so a purely relative comparison always fails. Set ",[14,459,460],{},"abs_tol"," to the size of rounding noise you expect for inputs of the magnitude being tested. Both tolerances together cover the full range: relative for large results, absolute for tiny ones.",[267,463,465,527],{"className":464},[270],[272,466,280,471,280,474,280,477,280,480,280,483,280,487,280,490,280,495,280,501,280,504,280,509,280,514,280,518,280,522],{"viewBox":467,"role":275,"ariaLabelledBy":468,"xmlns":279},"0 0 800 236",[469,470],"flt-t","flt-d",[282,472,473],{"id":469},"Relative and absolute tolerance together",[286,475,476],{"id":470},"A chart plots the allowed error against the magnitude of the expected result. The relative tolerance line grows with magnitude, while the absolute tolerance is a flat floor. Near zero the absolute floor dominates; for large results the relative line dominates. math.isclose accepts anything below the higher of the two.",[290,478],{"x":292,"y":292,"width":293,"height":479,"rx":295,"fill":296},"236",[298,481,482],{"x":300,"y":301,"textAnchor":302,"fontSize":303,"fontWeight":304,"fill":305},"Allowed error across magnitudes",[89,484],{"x1":485,"y1":486,"x2":311,"y2":486,"stroke":305,"strokeWidth":345},"80","196",[89,488],{"x1":485,"y1":486,"x2":485,"y2":489,"stroke":305,"strokeWidth":345},"50",[298,491,494],{"x":492,"y":493,"textAnchor":302,"fontSize":323,"fill":305},"410","222","|expected result|",[298,496,500],{"x":497,"y":498,"textAnchor":302,"fontSize":323,"fill":305,"transform":499},"66","124","rotate(-90 66 124)","allowed error",[89,502],{"x1":485,"y1":486,"x2":311,"y2":309,"stroke":344,"strokeWidth":503},"2.4",[298,505,508],{"x":506,"y":507,"fontSize":323,"fill":348},"600","78","rel_tol x |result|",[89,510],{"x1":485,"y1":511,"x2":311,"y2":511,"stroke":318,"strokeWidth":512,"strokeDashArray":513},"170","2.2",[328,342],[298,515,517],{"x":506,"y":516,"fontSize":323,"fill":324},"162","abs_tol floor",[290,519],{"x":520,"y":333,"width":310,"height":369,"rx":328,"fill":317,"opacity":521},"84","0.8",[298,523,526],{"x":524,"y":525,"textAnchor":302,"fontSize":334,"fill":324},"144","176","near zero",[381,528,529],{},"Without the absolute floor, every property whose true answer is zero fails on rounding noise alone.",[44,531,533],{"id":532},"nan-infinities-and-signed-zero-as-contract-decisions","NaN, infinities and signed zero as contract decisions",[10,535,536],{},"For each special value, the function should do one of three things, and the tests should say which.",[10,538,539,543,544,547,548,551],{},[540,541,542],"strong",{},"Reject it."," Raise a clear exception, as the ",[14,545,546],{},"mean"," above does for NaN. Test with a strategy that always includes the value, and ",[14,549,550],{},"pytest.raises",". Rejection is usually right for inputs that indicate upstream corruption — a NaN in a price list is a bug somewhere else.",[10,553,554,557,558,561],{},[540,555,556],{},"Propagate it."," Return NaN or infinity following IEEE semantics, as NumPy does. Test that the output is NaN with ",[14,559,560],{},"math.isnan"," rather than equality, and that finite inputs never produce NaN. Propagation is right for low-level numerical libraries whose callers expect IEEE behaviour.",[10,563,564,567],{},[540,565,566],{},"Exclude it from scope."," Configure the strategy never to generate it and document the precondition. This is the weakest choice, because production inputs are not bound by your strategy, and it is appropriate only where an earlier validation layer guarantees the value cannot arrive.",[10,569,570,571,574,575,578,579,582,583,586,587,589,590,435,593,596,597,64],{},"Signed zero deserves a special mention because it is invisible to equality: ",[14,572,573],{},"-0.0 == 0.0"," is true, but ",[14,576,577],{},"math.copysign(1, -0.0)"," is ",[14,580,581],{},"-1.0",", and ",[14,584,585],{},"1 \u002F -0.0"," raises in Python while producing ",[14,588,27],{}," in NumPy. Code that uses the sign of a result — angle calculations, direction of rounding — can behave differently for the two zeros. If that matters, compare with ",[14,591,592],{},"math.copysign",[14,594,595],{},"struct.pack"," rather than ",[14,598,599],{},"==",[44,601,603],{"id":602},"a-worked-failure-variance-that-goes-negative","A worked failure: variance that goes negative",[10,605,606],{},"The classic numerical bug that Hypothesis finds within seconds is a one-pass variance computed as the mean of squares minus the square of the mean:",[78,608,610],{"className":80,"code":609,"language":82,"meta":83,"style":83},"def variance(xs):\n    n = len(xs)\n    return sum(x * x for x in xs) \u002F n - (sum(xs) \u002F n) ** 2\n",[14,611,612,617,622],{"__ignoreMap":83},[87,613,614],{"class":89,"line":90},[87,615,616],{},"def variance(xs):\n",[87,618,619],{"class":89,"line":96},[87,620,621],{},"    n = len(xs)\n",[87,623,624],{"class":89,"line":102},[87,625,626],{},"    return sum(x * x for x in xs) \u002F n - (sum(xs) \u002F n) ** 2\n",[10,628,629,630,633,634,637,638,641,642,64],{},"The property is simple — variance is never negative — and it fails on a shrunk example along the lines of ",[14,631,632],{},"[1e8, 1e8 + 1]",". Both terms are about ",[14,635,636],{},"1e16",", their true difference is ",[14,639,640],{},"0.25",", and the subtraction of two nearly equal large numbers leaves only a few correct bits. With slightly different values the result comes out as a small negative number, which is mathematically impossible and breaks any later ",[14,643,644],{},"sqrt",[10,646,647,648,651,652,655],{},"The fix is not a tolerance on the test. It is a numerically stable algorithm: Welford's online method, or a two-pass computation that subtracts the mean before squaring. After the change, the non-negativity property passes, and a second property — that the result matches ",[14,649,650],{},"statistics.pvariance"," (which uses exact fractions internally) to ",[14,653,654],{},"rel_tol=1e-9"," — confirms the accuracy improved rather than just the sign.",[10,657,658,659,662],{},"That sequence is the typical shape of floating-point property testing. An obviously true mathematical property fails, the shrunk input points at a magnitude where precision runs out, and the fix is a better algorithm rather than a looser assertion. Loosening the assertion would have hidden a bug that production data — timestamps in nanoseconds, prices in large units — would eventually hit. It is also worth keeping the shrunk input as a permanent ",[14,660,661],{},"@example"," on the property: the values that exposed the cancellation are exactly the ones a future refactor back to the one-pass formula would fail on, and pinning them means that regression is caught on every run rather than only when the random search happens to wander back into the same region of large, nearly equal numbers.",[267,664,666,733],{"className":665},[270],[272,667,280,672,280,675,280,678,280,681,280,684,280,691,280,696,280,699,280,703,280,709,280,711,280,716,280,720,280,724,280,727,280,730],{"viewBox":668,"role":275,"ariaLabelledBy":669,"xmlns":279},"0 0 800 226",[670,671],"flv-t","flv-d",[282,673,674],{"id":670},"Cancellation in a one-pass variance",[286,676,677],{"id":671},"Two large nearly equal quantities, the mean of squares and the square of the mean, are each about ten to the sixteen. Subtracting them leaves only a few significant bits, so the computed variance can be negative. A two-pass or Welford algorithm subtracts the mean first and keeps full precision.",[290,679],{"x":292,"y":292,"width":293,"height":680,"rx":295,"fill":296},"226",[298,682,683],{"x":300,"y":301,"textAnchor":302,"fontSize":303,"fontWeight":304,"fill":305},"Subtracting two huge numbers loses the answer",[290,685],{"x":686,"y":687,"width":688,"height":689,"rx":690,"fill":329,"stroke":330,"strokeWidth":312},"30","54","220","48","9",[298,692,695],{"x":368,"y":693,"textAnchor":302,"fontSize":694,"fill":305},"83","11.5","mean(x²) ≈ 1.0000000200e16",[290,697],{"x":686,"y":698,"width":688,"height":689,"rx":690,"fill":329,"stroke":330,"strokeWidth":312},"118",[298,700,702],{"x":368,"y":701,"textAnchor":302,"fontSize":694,"fill":305},"147","mean(x)² ≈ 1.0000000200e16",[298,704,708],{"x":705,"y":706,"fontSize":707,"fontWeight":304,"fill":305},"290","116","22","−",[290,710],{"x":366,"y":520,"width":373,"height":687,"rx":370,"fill":317,"stroke":318,"strokeWidth":319},[298,712,715],{"x":713,"y":714,"textAnchor":302,"fontSize":374,"fontWeight":304,"fill":324},"430","108","-2.0 (wrong)",[298,717,719],{"x":713,"y":718,"textAnchor":302,"fontSize":334,"fill":305},"126","true value 0.25",[290,721],{"x":722,"y":520,"width":723,"height":687,"rx":370,"fill":343,"stroke":344,"strokeWidth":319},"566","208",[298,725,726],{"x":355,"y":714,"textAnchor":302,"fontSize":374,"fontWeight":304,"fill":348},"Welford \u002F two-pass",[298,728,729],{"x":355,"y":718,"textAnchor":302,"fontSize":334,"fill":305},"subtract mean first",[298,731,732],{"x":300,"y":373,"textAnchor":302,"fontSize":323,"fill":305},"Property \"variance ≥ 0\" finds it; a tolerance would hide it.",[381,734,735],{},"The shrunk counterexample names the magnitude where precision runs out, which points straight at the unstable subtraction.",[44,737,739],{"id":738},"edge-cases-and-failure-modes","Edge cases and failure modes",[49,741,742,762,768,785,801],{},[52,743,744,754,755,757,758,761],{},[540,745,746,749,750,753],{},[14,747,748],{},"float32"," code tested with ",[14,751,752],{},"float64"," values."," NumPy code that stores ",[14,756,748],{}," rounds every generated double. Use ",[14,759,760],{},"st.floats(width=32)"," so generated values are exactly representable.",[52,763,764,767],{},[540,765,766],{},"Overflow in intermediate values."," Squares, sums and products overflow long before the inputs reach the float limit. Bound the strategy by the square root of the limit for anything that squares.",[52,769,770,773,774,777,778,780,781,784],{},[540,771,772],{},"Catastrophic cancellation."," ",[14,775,776],{},"a - b"," for nearly equal ",[14,779,69],{}," and ",[14,782,783],{},"b"," keeps few significant digits. Properties comparing such results need tolerances relative to the inputs, not the result.",[52,786,787,773,793,796,797,800],{},[540,788,789,792],{},[14,790,791],{},"hypothesis.extra.numpy"," arrays.",[14,794,795],{},"arrays(np.float64, shape, elements=st.floats(...))"," passes the element strategy through; forgetting ",[14,798,799],{},"elements"," generates the full float range including NaN.",[52,802,803,773,806,809],{},[540,804,805],{},"Sorting with NaN.",[14,807,808],{},"sorted"," with NaN produces order that depends on input position. Exclude NaN from any property involving ordering unless NaN ordering is the point.",[44,811,813],{"id":812},"frequently-asked-questions","Frequently Asked Questions",[10,815,816,819,820,822],{},[540,817,818],{},"Why does my float test fail with NaN != NaN?","\nIEEE 754 defines NaN as unequal to everything, including itself, so any equality assertion on a NaN result fails. Either exclude NaN from the strategy, decide the function should reject NaN and test that, or compare with ",[14,821,560],{}," on both sides.",[10,824,825,828,829,832],{},[540,826,827],{},"What tolerance should I use with math.isclose?","\nUse a relative tolerance based on how many operations the computation performs — ",[14,830,831],{},"1e-9"," is a common start for short double-precision computations — plus an absolute tolerance for results near zero, where relative error is meaningless.",[10,834,835,838,839,842],{},[540,836,837],{},"Should I generate subnormal floats?","\nYes, unless the code explicitly does not support them. Hypothesis generates subnormals by default because they are a common source of precision loss and performance surprises. Restrict with ",[14,840,841],{},"allow_subnormal=False"," only when that is a documented limitation.",[44,844,846],{"id":845},"related","Related",[49,848,849,855,862,869],{},[52,850,851,854],{},[69,852,853],{"href":71},"Advanced Property-Based Testing"," — property patterns and strategy design.",[52,856,857,861],{},[69,858,860],{"href":859},"\u002Fproperty-based-fuzz-testing-strategies\u002Fadvanced-property-based-testing\u002Fwriting-metamorphic-properties\u002F","Writing Metamorphic Properties"," — scaling and shifting relations for numerical code.",[52,863,864,868],{},[69,865,867],{"href":866},"\u002Fproperty-based-fuzz-testing-strategies\u002Fdesigning-strategies-for-domain-data\u002Fgenerating-dataframes-and-arrays-with-hypothesis-extras\u002F","Generating DataFrames and Arrays with Hypothesis Extras"," — float arrays and columns.",[52,870,871,875],{},[69,872,874],{"href":873},"\u002Fproperty-based-fuzz-testing-strategies\u002Fadvanced-property-based-testing\u002Fround-trip-properties-for-serializers-and-parsers\u002F","Round-Trip Properties for Serializers and Parsers"," — floats through encoders.",[10,877,878,879],{},"← Back to ",[69,880,853],{"href":71},[882,883,884],"style",{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":83,"searchDepth":96,"depth":96,"links":886},[887,888,889,890,891,892,893,894,895],{"id":46,"depth":96,"text":47},{"id":75,"depth":96,"text":76},{"id":386,"depth":96,"text":387},{"id":427,"depth":96,"text":428},{"id":532,"depth":96,"text":533},{"id":602,"depth":96,"text":603},{"id":738,"depth":96,"text":739},{"id":812,"depth":96,"text":813},{"id":845,"depth":96,"text":846},"Property-test numerical Python with st.floats: NaN, infinities, signed zero, subnormals and overflow, choosing tolerances, and writing properties that survive floating-point rounding.","md",{"slug":899,"type":900,"breadcrumb":901,"datePublished":902,"dateModified":902,"faq":903,"howto":910},"testing-numeric-code-with-floats-and-nan-edge-cases","article","Floats & NaN","2026-09-18",[904,906,908],{"q":818,"a":905},"IEEE 754 defines NaN as unequal to everything, including itself, so any equality assertion on a NaN result fails. Either exclude NaN from the strategy, decide the function should reject NaN and test that, or compare with math.isnan on both sides.",{"q":827,"a":907},"Use a relative tolerance based on how many operations the computation performs — 1e-9 is a common start for short double-precision computations — plus an absolute tolerance for results near zero, where relative error is meaningless.",{"q":837,"a":909},"Yes, unless the code explicitly does not support them. Hypothesis generates subnormals by default because they are a common source of precision loss and performance surprises. Restrict with allow_subnormal=False only when that is a documented limitation.",{"name":911,"description":912,"steps":913},"How to property-test floating-point code","Decide the supported float domain, generate it with st.floats, and write properties with tolerances that reflect the computation.",[914,917,920,923],{"name":915,"text":916},"Decide the domain","Choose whether NaN, infinities, signed zero and subnormals are accepted, rejected or out of scope.",{"name":918,"text":919},"Generate that domain","Configure st.floats with bounds, allow_nan, allow_infinity and allow_subnormal to match.",{"name":921,"text":922},"Test the rejected values","Assert that unsupported inputs raise clearly instead of propagating silently.",{"name":924,"text":925},"Compare with tolerances","Use math.isclose with relative and absolute tolerance chosen from the computation.","\u002Fproperty-based-fuzz-testing-strategies\u002Fadvanced-property-based-testing\u002Ftesting-numeric-code-with-floats-and-nan-edge-cases",{"title":5,"description":896},"property-based-fuzz-testing-strategies\u002Fadvanced-property-based-testing\u002Ftesting-numeric-code-with-floats-and-nan-edge-cases\u002Findex","iLctLLZMEtmboJyjuU1F2WYY0RGb2LvvCC7wzcgUP94",1789718767657]