Prevent Resource Leaks with Context Managers in Python

Flags files, sockets, database connections, locks, and temp files left open when code raises, including sqlite3 connections a with block does not close.

What it flags

Flaggedcatalog/import_prices.py
import csv
import sqlite3


def import_prices(db_path: str, csv_path: str) -> int:
    conn = sqlite3.connect(db_path)
    f = open(csv_path, newline="")
    # Any exception from here on skips both close() calls
    rows = [(row["price"], row["sku"]) for row in csv.DictReader(f)]
    conn.executemany("UPDATE products SET price = ? WHERE sku = ?", rows)
    conn.commit()
    f.close()
    conn.close()
    return len(rows)
Passescatalog/import_prices.py
import csv
import sqlite3
from contextlib import closing


def import_prices(db_path: str, csv_path: str) -> int:
    with open(csv_path, newline="") as f:
        rows = [(row["price"], row["sku"]) for row in csv.DictReader(f)]

    with closing(sqlite3.connect(db_path)) as conn:
        # conn's own context manager commits or rolls back; closing() closes it
        with conn:
            conn.executemany("UPDATE products SET price = ? WHERE sku = ?", rows)
    return len(rows)

Why it matters

CPython often closes an abandoned file once its last reference disappears, which hides these leaks in tests. That cleanup is an implementation detail: tracebacks, caches, and reference cycles delay it, PyPy defers it to the garbage collector, and it never removes a mkdtemp() directory or releases a held lock. Long-running workers then fail with too many open files, exhausted connection pools, or deadlocks that only appear under load.

Agent instructions

cubic checks every pull request against these instructions. Paste them into AGENTS.md or Cursor rules and your AI coding tool follows them while it writes code.

AGENTS.md

Add this section to AGENTS.md at the repository root. Codex, Cursor, and other AI coding tools read it.

## Prevent Resource Leaks with Context Managers in Python

Applies to `**/*.py`.

Review Python code that opens files, sockets, connections, locks, subprocesses, or temporary files for resources that are not released on every path.

Flag changes that:
- Call `open()`, `socket.socket()`, `zipfile.ZipFile()`, `tarfile.open()`, or a database `connect()` without a `with` block or a `try`/`finally` that closes it.
- Release a resource with a plain `.close()` call placed after code that can raise, so an exception skips it.
- Treat `with sqlite3.connect(...) as conn` or a psycopg2 `with conn:` block as if it closed the connection; both only commit or roll back, so wrap the connection in `contextlib.closing()` or close it explicitly.
- Call `lock.acquire()` or `semaphore.acquire()` without a guaranteed release; use `with lock:`.
- Create files or directories with `tempfile.mkstemp()`, `tempfile.mkdtemp()`, or `NamedTemporaryFile(delete=False)` without removing them in a `finally`, or leave the `mkstemp()` file descriptor open.
- Return a lazy object tied to a resource opened in a `with` block, such as `csv.reader(f)`, a generator over `f`, or a cursor, which fails once the block has closed the resource.
- Start `subprocess.Popen` without `wait()`, `communicate()`, or a `with` block, or create a `requests.Session` or `httpx.Client` per call without closing it.

Do not flag:
- Resources whose ownership passes to an object or caller that closes them, such as a class with `close()` or `__exit__`, a pytest fixture that yields and closes, or a framework-managed connection.
- Process-lifetime clients or pools created once at startup.
- `Path.read_text()`, `Path.write_bytes()`, and similar helpers that open and close the file themselves.

Checks files matching **/*.py.

More Python agents

See all Python code review standards

Run this agent on every pull request

cubic reviews every pull request against the agents you turn on and comments on the lines that break them.

Catch this in your code