I mean, you could take something as facile as the sha1 of the current microtime, and a random concatenation of the user's data from the user table and that would already require so much access that figuring out the token wouldn't even be your biggest problem.
I once collected a $3k bug bounty over this. Python's use of Mersenne Twister in the lib/random module should not be used for token generation. Mersenne twister uses a relatively small state space and is fully deterministic (it never re-seeds or mixes in new entropy). If you get a couple sequential random values you can reconstruct that state space and predict all future values. I.E. request a password reset 10x in a row and examine the tokens in the emails.
Please only use secure random number generators when creating security related tokens.
Example blog post: https://know.bishopfox.com/blog/2014/08/untwisting-mersenne-...
Edit: never mind, your article named it as a good choice
Other flaws exist like abysmally poor key spaces. If your prng has a period of 64k and the reset links are generated deterministically from the prng, you're going to have a bad time.
The sha1 of microsecond+userdata is interesting. It has the potential to work well, but it's easy to get wrong. Latency measurements, the framework you're using, and other pieces of information can reduce microsecond timings to a few bits of entropy (e.g. there are modern systems that can only measure time aligned to 15millisecond boundaries). Once you take out the PII (most of which the attacker has access to already, so it isn't buying additional entropy), in systems I've seen there isn't that much real entropy in user state (sometimes under 12 bits even with tens of millions of users), and users who haven't interacted with your system much will have much less. If your system is closed source you might buy some security through obscurity, but that never lasts, and the underlying crypto is _probably_ flimsy at best.
It wouldn't take that much effort to go through my claims and find special cases where the strategies would work well, find workarounds for the attacks mentioned, and whatnot. That isn't really the point though. What matters is that getting this right is hard, and even a system which looks good enough might have subtle flaws that render its security all but useless. Maybe if we went back and forth enough we'd find all the problems, but there are already battle-tested solutions that are almost certainly better than anything we're going to come up with here, and in any application where security matters, ignoring those drop-in solutions is probably the wrong choice.
That said, it might very well be the case that having a certain percentage of user accounts compromised is an acceptable trade-off (or even desirable? could you then charge people to monitor their accounts for suspicious activity à la Equifax?). I think that's a choice that should be made consciously though, not as an afterthought arising from a broken security model.
Which is how some people seem to approach security advice... "either it's up to my ideal standard, or it's a completely idiotic implementation that will surely be hacked in a fortnight."
You seem to have some balance and I applaud that. Security is a balancing act between the level of security, development and maintenance difficulty, and user experience and you have to negotiate an acceptable level that at least exceeds the bare minimum of security required.