Decorator to cache a function result for some time
Rather than using *args you can supply a default positional only argument.

def temporary_cache(fn=None, *, ttl=60):
    ...
    if fn is not None:
        return decorator(fn)
    return decorator

If you feel following "flat is better than nested" is best, we can use functools.partial to remove the need to define decorator.

def temporary_cache(fn=None, *, ttl=60):
    if fn is None:
        return functools.partial(temporary_cache, ttl=ttl)

    @functools.wraps(fn)
    def inner(*args, **kwargs):
    ...


  for debugging, disable in production


You can use logging for this. I will leave actually implementing this as an exercise.

  I also couldn't abstain from using the new walrus operator (Python 3.8+), since I'm always looking for opportunities to use it in order to get a better feel for it.


A very reasonable thing to do. Abuse the new feature until you know what not to do. +1

However, I don't think this is a good place for it.
Given all the brackets in such a small space I'm getting bracket blindness.
I can't tell where one bracket ends and the others starts.
I am not a fan of func.cache = ... and func.cache_time. You can stop assigning to a function by using nonlocal.


Bringing this all together, and following some of my personal style guide, gets the following. I'm not really sure which is better, but it's food for thought.

from datetime import datetime
import functools


def temporary_cache(fn=None, *, ttl=60):
    if fn is None:
        return functools.partial(temporary_cache, ttl=ttl)

    cache = None
    cache_time = datetime.fromordinal(1)

    @functools.wraps(fn)
    def inner(*args, **kwargs):
        nonlocal cache, cache_time
        now = datetime.now()
        if ttl &lt; (now - cache_time).total_seconds():
            cache = fn(*args, **kwargs)
            cache_time = now
        elif DEBUG:
            # for debugging, disable in production
            print("Cached", fn.__name__)
        return cache
    return inner