Creating branched narratives
I started reading at play. At first, I thought you had posted non-working code, because your code couldn't possibly work. Then I got up to the definition of Branch and realized that yes, it could work. But it violates so many principles that I don't know what the "real" type of a Branch is.

Let me suggest a slightly different model:


You have an outer loop. The loop ends when the narrative ends. You could express this as either an iteration stop, or as a boolean condition. I'm going to call each step in the narrative a step. You might prefer page or part or section, whatever helps you tell this story:

# Iteration stop:
for step in narrative:
    pass

# Boolean condition:
while not narrative.has_ended:
    step = narrative.current_step

Once you have a step, you can display the narrative text, and then present the user with a collection of options to choose from. Because users are users, there probably needs to be an inner loop that keeps asking for a valid option until one is entered. I would make a function for that:

    # Display the narrative text
    print(step.text)

    # Present the user with options, force a choice:
    choice = get_user_choice(step.options)



Finally, you have interacted with the user and received a choice, so update the narrative. Maybe your iterator has a .select method, or maybe your narrative does:

        step.select(choice)

        # or maybe

        narrative.branch(choice)


After looking at it, I personally prefer the idea of invoking methods on the Narrative. It shows more clearly that the narrative is an object, maintaining and updating its state. However, I would make the menu selection a "simple" cough! enhanced generator (see PEP-342):

from os import PathLike
from typing import Generator, Iterable, None, TextIO

def tell_a_story(datafile: PathLike, output: TextIO,
                 chooser: Generator[str, Iterable[str], None]):
    """
    Tell a branched-narrative story. Configure the narrative using
    "datafile", write the result on "output," interact with "chooser"
    (an enhanced generator) to select branches to follow.

    """
    narrative = Narrative(datafile)

    while not narrative.has_ended():
        step = narrative.current_step
        output.write(step.text)

        # send options to the generator, get a choice back
        choice = chooser.send(step.options)

        narrative.branch(choice)


I chose to use a Writable and an Extended Generator to help with automated testing. You should be able to supply a StringIO and a generator that ignores the options in favor of just iterating over a list of choices, and compare the generated text against what you predict for your test case.

If there is an epilog, you'll want to handle it specially. In particular, your generator should deal with an options parameter that is None. Then you could print the epilog, give the user no options, probably get None back from the generator, select None as your branch, and mark the narrative as ended.

Alternatively, you could create Narrative with some sort of .introduction text, print that outside the loop, and then loop in a different order:

output.write(narrative.introduction)

while not narrative.has_ended():
    choice = chooser(narrative.options)
    narrative.branch(choice)
    output.write(narrative.new_text)


This may tend to make your last loop iteration cleaner, and impose fewer constraints on your generator.

How Enhanced Generators Work

Generators are functions in Python that make use of the yield or yield from statements. We'll focus on yield because that's all we need.

You can create a generator like this:

def g():
    yield 1
    yield 2


You know it's a generator because you see yield. Twice!

Generators don't return a value when you "call" them. Instead, they return a generator object. It's like an iterator - it's an object that you can call next() on and it might return multiple values:

for x in g():
    print(x)


This does what you might expect: it prints

1 
2


So you can see that the yield keyword is used to pass data out of a generator, and suspend execution. When the caller invokes next(generator_object) or generator_object.__next__(), execution resumes where it left off - at the yield statement, and more code is executed. 

In a first attempt at supporting coroutines, python added generator.send() methods. This allows the caller to push data into the generator, and it works like this:

def eg():
    x = 0
    while True:
        x = yield x + 1

egen = eg()
p = egen.send(None)

for _ in range(5):
    print("P is:", p)
    p = egen.send(p + 3)


The output looks like:

P is: 1
P is: 5
P is: 9
P is: 13
P is: 17


As you can see, this lets you send data into the generator, then pull data back out. You just have to start off by sending a None to "prime the generator". 

This is relevant to you because you need to have a 2-way conversation. You want to send out some options, and get back a selection. You could do it like this:

def get_selection(choices):
    print("\n".join(choices))
    return input("What will it be?")


But then how would you test? You'd have to use global variables or something, and where's the fun in that? 

But if you built a generator, you could ignore the options and just pre-program a path through the data:

def test_selections(test_data):
    for choice in test_data:
        options = yield choice

chooser = test_selections(["a", "b", "a", "b", "a", "c"])
outfile = io.StringIO()
datafile = "story.txt"

tell_a_story(datafile, outfile, chooser)


Or, you could talk to the user:

def user_selections():
    choice = None
    while True:
        options = yield choice
        print("\n".join(options))
        choice = input("What will it be?")


Depending on whether you want to test or really use it, you just pass a different generator.

Edit (responding to changes in the original question):

First, note that etiquette on CodeReview is to append your updates, not edit them in-place. When you make in-place changes, that invalidates the answers people posted and makes other readers confused.

That said, this version looks considerably better. My main suggestion would be to remove the enumerate from getoptions (let the display code handle the enumeration), and move getoptions into Branch as a method rather than a free function. When you do that, of course, you realize that you could just let the caller access Branch.options directly, which is fine. ;-)

One other suggestion is to make the story an incoming parameter for play(), like:

if __name__ == '__main__':
    play(Story_main)