> if the default value is expensive to construct and rarely needed:

I'd say "or" rather than "and": defaultdict has higher overhead to initialise the default (especially if you don't need a function call in the setdefault call) but because it uses a fallback of dict lookup it's essentially free if you get a hit. As a result, either a very high redundancy with a cheap default or a low amount of redundancy with a costly default will have the defaultdict edge out.

For the most extreme case of the former,

    d = {}
    for i in range(N):
        d.setdefault(0, [])
versus

    d = defaultdict(list)
    for i in range(N):
        d[0]
has the defaultdict edge out at N=11 on my machine (561ns for setdefault versus 545 for defaultdict). And that's with a literal list being quite a bit cheaper than a list() call.