How to free up memory in a CircuitPython board?
I love Python.  Most of the programs I write for my own use on my PC at home are written in Python.

That said, I don't use Python in my microcontroller based projects.  

Python is intended for use where you have memory and processor horsepower behind it.  It dynamically allocates memory, and takes care of cleaning up objects your program doesn't need anymore.

All of that makes Python a poor choice for use on small systems with limited RAM.

CircuitPython presumably tries to get around those limitations, but as you've discovered there are limits to how far that goes.

How you write your software will influence how much RAM your program uses.  A change in style so as to prevent wasted RAM will probably be more effective than trying to clean up after a messy program.

You should try to never create new objects while your program is running.

Your biggest culprits are probably strings.  Python strings are immutable.  If you try to change one, what happens in the background is that a new string is created and assigned to the original variable - the old string stays in memory until the garbage collector gets it.  If you are doing any string operations at all, they can easily add up.  Worse, they can slice the available RAM into little pieces such that while the total free space may be large enough there might be no place large enough in one piece to hold your string.  That'll cause an exception just as surely as when there's no free RAM left at all.

That applies to all objects.  Lists, arrays, dictionary, class instances, etc. will all have similar problems.

About the only thing you can trust to not dynamically allocate storage are integers.  You'll want to work using integers as much as possible.  Read bytes from the RTC, and store them as integers.  Use integers to do any processing you need to do in order to generate the clock display data, and use integers while writing to the display.

If you stick to just those things that will work reliably without wasting RAM, then you'll find that most of the things that make Python nice can't be used on your little processor - but, you'll still have all those nice things there while you are programming and they will tempt you to use them.  You have to continuously remember what things you can't use.  It makes programming tougher.

The idea behind using Python for microprocessors seems to be to make it easier for beginners to get started.  It does, sort of.  At the beginning, you have a nice flat easy to handle learning curve - until you hit a brick wall that will require either massive tricks to get around or require learning a new language.

Better to switch to a language that works well on the microcontroller to start with.  Programs on a microcontroller are generally small, with fairly limited functionality.  You don't need all the niceness of Python if all you are doing is reading a few bytes from one piece of hardware and writing a few bytes to another piece of hardware - and that's really all your clock program does.

There's IDEs for most microcontroller families available.  They usually include assistance for working directly with the hardware ports.  Pick a system you like the looks of, and learn to use it.

The Arduino and related IDE work well for beginners (though it has some traps of its own.)  There are plenty of alternatives out there, though.