This rule raises an issue when a @contextmanager generator performs cleanup after yield without guaranteeing that cleanup still runs if the generator exits early.

Why is this an issue?

@contextmanager turns a generator into a context manager. The code before yield runs on enter, the with body runs at the yield, and the code after yield is meant to clean up on exit.

If cleanup sits after yield without a try/finally (or a nested with), it does not run when:

In those cases, resources such as files, locks, or connections can stay open.

from contextlib import contextmanager

@contextmanager
def temporary_file(path):
    handle = open(path, "w")
    yield handle
    handle.close()  # Noncompliant: skipped if the with-block raises

If statements after yield are not cleanup (for example optional logging that is safe to skip), or cleanup is already guaranteed by a nested with or an enclosing try/finally the analyzer failed to see, treat that as a false positive. Marking a real resource release as won’t-fix is wrong: exceptions and early generator close can still skip those statements and leak the resource.

What is the potential impact?

Missed cleanup can leak resources, leave locks held, or leave shared state inconsistent. These failures often show up only under exceptions or early generator shutdown, which makes them harder to reproduce.

How to fix it

Wrap the yield in try/finally so cleanup always runs, or nest resource acquisition in a with statement so the inner context manager owns the cleanup.

Code examples

Noncompliant code example

from contextlib import contextmanager

@contextmanager
def temporary_file(path):
    handle = open(path, "w")
    yield handle
    handle.close()  # Noncompliant: cleanup after "yield" may be skipped

Compliant solution

from contextlib import contextmanager

@contextmanager
def temporary_file(path):
    handle = open(path, "w")
    try:
        yield handle
    finally:
        handle.close()

Noncompliant code example

from contextlib import contextmanager

@contextmanager
def locked_section(lock):
    lock.acquire()
    yield
    lock.release()  # Noncompliant: "release" is skipped on exception

Compliant solution

from contextlib import contextmanager

@contextmanager
def locked_section(lock):
    with lock:
        yield

How does this work?

When an exception escapes the with body, Python resumes the generator at yield and re-raises there. A finally block still runs in that situation, and so does __exit__ for a nested with. Plain statements after yield do not.

Resources

Documentation

External coding guidelines