I generally conflate fuzz and property based testing, particularly when I nearly always mean property based testing. This is lazy, and is lazily justified with "fuzzing is pbt with very generic inputs / pbt is fuzzing with more intelligent inputs".
Regardless, the hard part with pbt is generating the inputs so they cover real cases and dont accidentally systematically miss some (this is often achieved by choosing different testing boundaries). The hard part with fuzzing is knowing you've done enough that you've hit many real data flows.
My understanding is that while the how is similar (both throw mountains generated input at the system), there's a qualitative difference in that pbt asserts that some fact holds true of the output, while fuzzing "just" tries to make it crash. Put another way, fuzzing might be described as testing the property that "the system under test doesn't crash". In practice fuzz testing is a special enough case that it makes sense to break it out into its own thing with its own tools.
I think there's another difference between the two that isn't there by definition but manifests in practice:
If we were to imagine a simplified landscape in which all tests fall on a one-dimensional axis, from "quick unit tests" on the left to "slow integration tests" on the right , fuzzing is even further off to the right.
Fuzzing is often very slow and operates on an entire black-box program, trying to understand and break it. Access to source code isn't always needed.
My understanding may be incomplete; please correct me if I missed something.
There's a lot of overlap. In fuzzing you generally generate inputs that exercise the underlying (hidden) branching conditions. Some explicit, some implicit.
In PBT you generate inputs that exercise branching and value range conditions for a known subset of target variables or other internal states. The targets being tested are explicit.
Or, to quote David on his visit to our office: "Hypothesis is basically a python bytecode fuzzer."
Regardless, the hard part with pbt is generating the inputs so they cover real cases and dont accidentally systematically miss some (this is often achieved by choosing different testing boundaries). The hard part with fuzzing is knowing you've done enough that you've hit many real data flows.