python-hypothesis

Why use recursive() instead of deferred()?


I needed a strategy for arbitrary JSON values and after reading about the gotchas of using composite() for recursive data came up with this

json_primitives = st.one_of(
    st.none(),
    st.booleans(),
    st.integers(),
    st.floats(allow_infinity=False, allow_nan=False),
    st.text(),
)

def json_collections(values):
    return st.one_of(
        st.dictionaries(keys=st.text(), values=values),
        st.lists(values),
    )

json_values = st.recursive(json_primitives, json_collections)

In the tests of hypothesis itself I found something like

json_values = st.deferred(
    lambda: st.none()
    | st.booleans()
    | st.integers()
    | st.floats(allow_infinity=False, allow_nan=False)
    | st.text()
    | json_arrays
    | json_objects
)
json_arrays = st.lists(json_values)
json_objects = st.dictionaries(st.text(), json_values)

Are there any differences in how these strategies behave? I looked at the implementations of both and found the one for st.deferred much easier to follow. And I arguably find the use of deferred easier to read as well (even without the bitwise or syntactic sugar for st.one_of)


Solution

  • Are there any differences in how these strategies behave?

    Nope - use whichever is more convenient or clearly expresses your intent.


    So... why have both then? The answer is partly just historical contingency: back in 2015 Hypothesis had an entirely different backend which could support st.recursive() but not st.deferred(), which was added shortly after David wrote the current backend in 2017.

    We've kept both because one or the other are more natural in some situations or to some people, and deprecating something as widely used and non-broken as st.recursive() would be far more painful than keeping it.

    In practice you can also cleanly use st.recursive() in an expression, defining the whole thing inline with extend=lambda s: .... I've never seen it in the wild, but I guess you could use an assignment expression to do the same...

    @given(st.recursive(st.booleans(), st.lists))
    def test_demo_recursive(x):
        ...
    
    @given(xs := st.deferred(lambda: st.booleans() | st.lists(xs)))
    def test_demo_deferred(x):
        ...
    

    Not sure how this would look in more complex cases and of course it requires Python 3.8+, but the latter is nicer than I expected.