Tower of Hanoi with helper function
Overall your implementation is fine, I would just suggest a few things:


Naming - move_rings and helper are not very descriptive. Give them better names, and give them good docstrings (I like NumpyDoc, but personal preference is fine)
move_rings shouldn't both do calculations and print to stdout. Instead, move_rings should return the information, and the caller should decide what to do with it
move_rings doesn't need the number_rings == 1 special case; your API handles it just fine
Instead of the hard-coded number approach, you can make a little Peg class that makes things a bit cleaner - subclassing namedtuple is a nice way to do this
Using f-strings makes printing easier
You can yield from instead of appending to the default argument list, which is easier to read and doesn't waste time reallocating space for the list
I added a (honestly pretty bad) visualization function to see the movements as well.


This is what I came up with (didn't do all of the things I mentioned above)

from math import ceil
from collections import defaultdict, namedtuple
from typing import Iterable, Generator, List, Tuple


class Peg(namedtuple("Peg", "id name")):
    pass


SOURCE_PEG = Peg(0, "A")
TARGET_PEG = Peg(2, "C")
WORKER_PEG = Peg(1, "B")
PEGS = [SOURCE_PEG, TARGET_PEG, WORKER_PEG]


def helper(
    number_rings: int, origin: Peg, target: Peg, spare: Peg
) -&gt; Generator[Tuple[Peg, Peg], None, None]:
    """
    Helper function for performing the recursive call when solving the tower puzzle.

    Parameters
    ----------
    number_rings : int
        Number of rings in the puzzle
    origin : Peg
        The peg that we're moving from
    target : Peg
        The peg that we want to move to
    spare : Peg
        The other peg, that we can use as a helper

    Yields
    ------
    move : tuple
        The movement(s) (from-Peg, to-Peg) we determined were ncessary
    """

    if number_rings == 1:
        yield (origin, target)
    else:
        # Move all but 1 ring from ORIGIN tower to SPARE tower
        yield from helper(
            number_rings - 1,
            origin=origin,
            target=spare,
            spare=target,
        )
        # MOVE the last ring from ORIGIN tower to TARGET tower
        yield from helper(
            1, origin=origin, target=target, spare=spare
        )
        # MOVE the remaining rings from the SPARE tower to TARGET tower
        yield from helper(
            number_rings - 1,
            origin=spare,
            target=target,
            spare=origin,
        )

def move_rings(
    number_rings: int
) -&gt; Iterable[Tuple[Peg, Peg]]:
    """
    Finds the optimal number of moves when solving a Tower of Hanoi puzzle
    with `number_rings` rings, starting on the first ring.

    Parameters
    ----------
    number_rings : int
        The number of rings in the Tower of Hanoi puzzle

    Returns
    -------
    move_list : iterator
        An iterable of the Peg movements
    """

    return helper(
        number_rings, SOURCE_PEG, TARGET_PEG, WORKER_PEG
    )


def draw_tower_of_hanoi(ring_map: List[int]):
    """
    Pretty dumb drawing tool that visualizes current peg state.

    Parameters
    ----------
    ring_map : list
        List of rings and their pegs. Format for a 3-ring puzzle start would be
        [0, 0, 0]. The index represents ring size (size = i+1), and the value at
        that index represents the peg (0, 1, or 2).
    """

    largest_ring = len(ring_map)
    peg_to_ring = defaultdict(list)
    for ring, peg in enumerate(ring_map, 1):
        peg_to_ring[peg].append(ring)

    peg_to_ring = {
        peg: sorted(ring_list, reverse=True)
        for peg, ring_list in peg_to_ring.items()
    }
    height_per_ring = {
        peg: len(ring_list)
        for peg, ring_list in peg_to_ring.items()
    }

    total_width = largest_ring * 6 + 7
    total_height = len(ring_map) + 2

    peg_centers = [
        1 + largest_ring,
        3 + largest_ring * 3,
        5 + largest_ring * 5,
    ]

    # fill it out
    drawing = [
        [" " for _ in range(total_width)]
        for _ in range(total_height)
    ]

    # Draw the pegs
    for i, row in enumerate(drawing):
        for center in peg_centers:
            drawing[i][center] = "P"

    # Draw the base
    drawing[-1][:] = ["G"] * total_width

    # Draw the rings
    for peg in peg_to_ring:
        for i, ring in enumerate(peg_to_ring[peg], 2):
            index = -i
            center = peg_centers[peg]
            drawing[index][center - ring : center] = [
                "R"
            ] * ring
            drawing[index][
                center + 1 : center + ring + 1
            ] = ["R"] * ring

    print("\r\n".join(["".join(row) for row in drawing]))



def visualize_tower_movements(
    number_rings: int, moves: Iterable[Tuple[Peg, Peg]]
):
    """
    Visualization tool for drawing the movement of rings in a Tower of Hanoi puzzle.

    Parameters
    ----------
    number_rings : int
        The number of rings in the puzzle
    moves : iterable
        The set of moves performed while working on the puzzle
    """

    visualizer = [0] * number_rings
    draw_tower_of_hanoi(visualizer)
    for move_from, move_to in moves:
        # Get the smallest ring on the peg, as that is the only one that can move
        move_from_index = min(
            ring
            for ring, value in enumerate(visualizer)
            if value == move_from.id
        )
        visualizer[move_from_index] = move_to.id
        draw_tower_of_hanoi(visualizer)



if __name__ == "__main__":
    moves = list(move_rings(5))
    print(f"Took {len(moves)} moves")
    for move_from, move_to in moves:
        print(
            f"Move from {move_from.name} to {move_to.name}"
        )
    visualize_tower_movements(5, moves)