How do I ensure that a generator gets properly closed?

As my comment mentioned, one way to properly structure this would be using the contextlib.contextmanager to decorate your generator:

from typing import Iterator
import contextlib

@contextlib.contextmanager
def get_numbers() -> Iterator[int]:
    acquire_some_resource()
    try:
        yield iter([1, 2, 3])
    finally:
        release_some_resource()

Then when you use the generator:

with get_numbers() as et:
    for i in et:
        if i % 2 == 0:
            raise ValueError()
        else:
            print(i)

Result:

generating some numbers
1
done generating numbers
Traceback (most recent call last):
  File "<pyshell#64>", line 4, in <module>
    raise ValueError()
ValueError

This allows the contextmanager decorator to manage your resources for you without worrying handling the release. If you're feeling courageous, you might even build your own context manager class with __enter__ and __exit__ function to handle your resource.

I think the key takeaway here is that since your generator is expected to manage a resource, you should either be using the with statement or always be closing it afterwards, much like f = open(...) should always follow with a f.close()