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
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)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.