What Went Wrong
The issue is that at you have put yield statements in exception handlers. This means that the code can break out of both your while loops and terminate the coroutine. This happens when you throw() an exception in the coroutine when it has just yielded from one of statements that are in exception handlers, but without their own try/catch. Pretty sure, this is not what you intended.
In your example, what is happening is that when you send 'banana' you cause a KeyError in the generator. So when you next do coro.throw(KeyError), you in the inner most exception handler for KeyError. Because there is no additional exception handling, the code breaks out of the inner while loop. We then catch the KeyError in the exception handler for the outer loop and start the outer while loop again. This means that the next yield from the generator will be letter = yield -- the implicit yielding of None. I've annotated your code to try and show what happens. Start at # 1: and follow the numbers until # 6:
def alphabet():
while True:
try:
# 6: next yield after coro.throw(KeyError). So when do we do coro.send('dog'),
# the coro yields None
letter = yield #waiting for first input from send
while True:
try:
# 1: coro.send('banana') sets letter to banana -- no exception
# 2: inner while loop starts again, does `DICTIONARY['banana'] and gets
# a KeyError
letter = yield DICTIONARY[letter]
except KeyError:
# 3: yield 'default' after KeyError('banana')
# 4: coro.throw(KeyError) causes this statement to raise a KeyError
letter = yield 'default'
except Exception:
letter = yield 'default'
except KeyError:
# 5: KeyError raised at `4` handled here
# Had you done throw(KeyError) a second consecutive time, this statement would
# have raise an exception, and then terminated your coroutine.
letter = yield 'default'
except Exception:
letter = yield 'default'
Fixing the Problem
You can get your desired behaviour by only ever doing yield statements in a try/except. You can prevent the need to nest while loops by pulling apart the two things (yield and DICTIONARY[letter]) that can cause exceptions and putting them in their own try/excepts.
def alphabet():
next_result = None
while True:
try:
letter = yield next_result
except Exception as ex:
# yield statement raised an exception -- letter is unchanged
print(f'{ex!r} was raised')
next_result = 'default-after-exception'
else:
# yield statement set letter to a new value
next_result = DICTIONARY.get(letter, f'default-{letter}-is-missing')
## alternatively:
# try:
# next_result = DICTIONARY[letter]
# except KeyError:
# next_result = 'default'
coro = alphabet()
assert next(coro) is None
assert coro.send('apple') == 'default-apple-is-missing'
assert coro.send('banana') == 'default-banana-is-missing'
assert coro.throw(KeyError) == 'default-after-exception'
assert coro.send('dog') == 'default-dog-is-missing'
assert coro.send('d') == 'dog'
try. You already catch any possible exceptions with the innertry. It's just protectingletter = yieldwhich can't raise an exception.DICTIONARY.get(letter, 'default')rather than catchingKeyError.default, they're actually gettingNone.