hunter-party-py/perf-hunter-py/SKILL.md
Audit Python code for performance antipatterns and resource management issues — blocking I/O in async contexts, sequential awaits, N+1 queries, unclosed resources, unbounded caches, eager materialization, missing connection pooling, and expensive operations in hot paths. Use when: reviewing async correctness, auditing resource lifecycle, hunting N+1 query patterns, checking connection pool configuration, or profiling structurally inefficient code.
npx skillsauth add skyosev/agent-skills perf-hunter-pyInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Audit Python code for performance antipatterns and resource management — places where I/O blocks the event loop, independent operations run sequentially instead of concurrently, queries multiply inside loops, resources leak without cleanup, caches grow without bound, or expensive work sits in hot paths. The goal: every async function is truly non-blocking, every resource has a bounded lifecycle, and every data access pattern scales with the workload.
Async means non-blocking. An async def that calls blocking I/O is worse than sync — it blocks the entire
event loop, stalling all concurrent coroutines. Every I/O operation inside an async function must use an async API.
Concurrency is the point. Independent async operations should run concurrently. Sequential await on
independent calls wastes the concurrency that async provides.
Batch over loop. One query returning N results is almost always faster than N queries returning 1 result each. The network round-trip dominates, not the query complexity.
Resources must be bounded. Every cache needs eviction, every pool needs a size limit, every buffer needs backpressure. Unbounded growth is a slow memory leak that only manifests under production load.
Lazy over eager. Don't materialize a million-item list to iterate over it once. Generators, iterators, and streaming APIs exist for a reason — use them for large datasets.
Pool and reuse. Creating connections, clients, and sessions is expensive. Pool them at the application level and reuse across requests.
Profile before optimizing. Not every hot path needs optimization. Flag patterns that are structurally inefficient (N+1, blocking I/O in async, unbounded caches) rather than speculating about micro-performance.
Not every finding requires action. Document these but do not flag as "must-fix":
| Pattern | When Acceptable |
| ------- | --------------- |
| Blocking I/O in async def | Wrapped in asyncio.to_thread() or run_in_executor() |
| Sequential awaits | When call B depends on result of call A |
| list() materialization | When random access, len(), or multiple iterations are needed |
| Per-request client creation | One-off scripts, CLIs, or test utilities |
| @lru_cache(maxsize=None) | Pure functions with bounded input domains (e.g., enum→string mapping) |
| Module-level mutable state | Application registries populated once at startup |
Synchronous I/O calls inside async def functions that block the event loop, defeating the purpose of async.
Signals:
open() inside async defrequests.get() / requests.post() inside async deftime.sleep() inside async def (should be asyncio.sleep())cursor.execute()) inside async defsubprocess.run() inside async def (should be asyncio.create_subprocess_exec())Action: Replace with async equivalents: aiofiles.open(), httpx.AsyncClient, asyncio.sleep(), async database
drivers, asyncio.create_subprocess_exec(). If no async equivalent exists, use asyncio.to_thread() or
loop.run_in_executor().
Multiple independent await calls made sequentially when they could run concurrently with asyncio.gather() or
asyncio.TaskGroup.
Signals:
await calls where neither depends on the result of the other:
a = await fetch_a(); b = await fetch_b(); c = await fetch_c()Action: Use asyncio.gather() or asyncio.TaskGroup (3.11+) for independent async operations. Keep sequential
for dependent calls.
Code that issues one database query per item in a loop instead of a single batch query.
Signals:
for item in items: await repo.find_by_id(item.id) — loop with individual queriesfor user in users: user.orders = await get_orders(user.id) — lazy loading in a loopSELECT ... WHERE id = ? called N times in a loop instead of SELECT ... WHERE id IN (?)Action: Replace with batch queries (WHERE id IN (...)), use ORM eager loading (joinedload, selectinload in
SQLAlchemy), or prefetch related data before the loop.
Files, connections, sessions, or client objects opened without proper cleanup, risking resource leaks.
Signals:
f = open(...) without with statementsession = Session() without context managerclient = httpx.AsyncClient() without async withconn = await pool.acquire() without try/finally or context managertempfile.NamedTemporaryFile(delete=False) without cleanup logicAction: Wrap in context managers (with/async with). For resources that span scopes, use
contextlib.AsyncExitStack or register cleanup in finally / atexit.
Collections that grow without limit: caches without eviction, lists that accumulate indefinitely, or queues without backpressure.
Signals:
cache = {} populated in a function called repeatedly with no evictionresults = [] appending in an infinite loop or long-running server@lru_cache without maxsize (defaults to 128, but @lru_cache(maxsize=None) is explicitly unbounded)deque() without maxlen used as a bufferAction: Use @lru_cache(maxsize=N) or cachetools.TTLCache with TTL and size limits. For buffers, use
collections.deque(maxlen=N). For event logs, use rotation. Prefer weak references
(weakref.WeakValueDictionary) when caching objects.
List comprehensions or list() calls that materialize large datasets into memory when a generator or iterator would
suffice.
Signals:
list(range(1_000_000))[x for x in huge_query_result] followed by iterationdata = list(reader) when processing line-by-line would worksorted(huge_list) on data only partially consumedreadlines() instead of iterating the file object directlyAction: Use generator expressions (x for x in ...), iterate directly over iterables, use itertools.islice()
for partial consumption, process files line-by-line with for line in f:.
Database or HTTP connections created per-request without pooling, or pool defaults that are too small/large.
Signals:
create_engine() without pool_size, pool_pre_ping, or pool_recycle argshttpx.Client() or requests.Session() created inside request handlers (per-request)aiohttp.ClientSession() created inside a looppsycopg2.connect() or asyncpg.connect() without pool wrapperpool_size or max_overflow tuning for SQLAlchemy enginesAction: Use connection pooling (create_async_engine(pool_size=N, max_overflow=M)). Reuse HTTP clients as
application-scoped singletons. Use asyncpg.create_pool() instead of individual connections.
Computationally expensive or I/O operations placed inside tight loops, request handlers, or frequently-called functions without caching or batching.
Signals:
re.compile() inside a function called in a loop (should be module-level constant)json.loads()/json.dumps() on the same data multiple timesos.path.exists() / Path.exists() checks on the same path in one requestimportlib.import_module() in a hot pathstr.join() or io.StringIOAction: Hoist expensive operations out of loops. Pre-compile regexes at module level. Cache expensive computations with appropriate TTL. Use batching for I/O operations.
main/master)BASE=$(git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed 's@^refs/remotes/origin/@@' || echo main)
SCOPE=$(git diff --name-only $(git merge-base HEAD $BASE)...HEAD)
Constrain all subsequent scans to the resolved surface.EXCLUDE='--glob !**/*_test.py --glob !**/test_*.py --glob !**/tests/** --glob !**/venv/** --glob !**/.venv/**'
# Blocking I/O in async context
rg 'async def' --type py $EXCLUDE -l | head -20
rg --pcre2 'async def[\s\S]*?(?:open\(|requests\.|time\.sleep|subprocess\.run)' --type py $EXCLUDE
rg 'requests\.(get|post|put|delete|patch)\(' --type py $EXCLUDE
rg 'time\.sleep\(' --type py $EXCLUDE
# Sequential awaits
rg -U 'await.*\n\s*\w+\s*=\s*await' --type py $EXCLUDE
# N+1 patterns (loop with await/query inside)
rg -U 'for\s+\w+\s+in\s+\w+.*:\s*\n\s*.*await' --type py $EXCLUDE
rg -U 'for\s+\w+\s+in\s+\w+.*:\s*\n\s*.*\.execute\(' --type py $EXCLUDE
# Unclosed resources
rg --pcre2 '(?<!with\s)\bopen\(' --type py $EXCLUDE
rg 'Client\(\)' --type py $EXCLUDE
rg 'Session\(\)' --type py $EXCLUDE
# Unbounded caches
rg 'lru_cache\(maxsize=None\)|@lru_cache\s*$' --type py $EXCLUDE
rg --pcre2 'cache\s*[:=]\s*\{\}' --type py $EXCLUDE
# Eager materialization
rg 'readlines\(\)' --type py $EXCLUDE
rg 'list(range\(' --type py $EXCLUDE
# Connection pool configuration
rg 'create_engine\(|create_async_engine\(' --type py $EXCLUDE
rg 'pool_size|pool_pre_ping|max_overflow' --type py $EXCLUDE
# Hot path operations
rg 're\.compile\(' --type py $EXCLUDE
For each async function identified in Phase 2:
asyncio.to_thread() or run_in_executor() wrapping for unavoidable blocking calls.For each opened resource:
try/finally, or atexit.Save as YYYY-MM-DD-perf-hunter-audit-{$LLM-name}.md in the project's docs folder (or project root if no docs
folder exists).
# Perf Hunter Audit — {date}
## Scope
- Surface: {diff / path / codebase}
- Files: {count or list}
- Runtime: {async (FastAPI/aiohttp) / sync (Flask/Django) / mixed}
- Exclusions: {list}
## Async Architecture
- Framework: {FastAPI / aiohttp / Django async / Flask / sync-only}
- Event loop: {uvloop / default asyncio / N/A}
- Async I/O libraries: {httpx / aiofiles / asyncpg / etc.}
## Findings
### Blocking I/O in Async Context
| # | Location | Blocking Call | Async Equivalent | Severity |
| - | -------- | ------------- | ---------------- | -------- |
| 1 | file:line | `requests.get()` in `async def` | `httpx.AsyncClient.get()` | High |
### Sequential Awaits
| # | Location | Calls | Independent? | Estimated Speedup | Action |
| - | -------- | ----- | ------------ | ------------------ | ------ |
| 1 | file:line | `fetch_a`, `fetch_b`, `fetch_c` | Yes | ~3x | Use `asyncio.gather()` |
### N+1 Query Patterns
| # | Location | Loop | Query per Iteration | Batch Alternative |
| - | -------- | ---- | ------------------- | ----------------- |
| 1 | file:line | `for user in users` | `get_orders(user.id)` | `get_orders_batch(user_ids)` |
### Unclosed Resources
| # | Location | Resource | Cleanup | Action |
| - | -------- | -------- | ------- | ------ |
| 1 | file:line | `open()` | No `with` | Wrap in context manager |
### Unbounded In-Memory Growth
| # | Location | Collection | Growth Pattern | Action |
| - | -------- | ---------- | -------------- | ------ |
| 1 | file:line | `cache = {}` | Module-level dict, never evicted | Use `TTLCache(maxsize=1000, ttl=300)` |
### Eager Materialization
| # | Location | Operation | Data Size | Action |
| - | -------- | --------- | --------- | ------ |
| 1 | file:line | `list(reader)` | Unbounded file | Iterate directly |
### Missing Connection Pool Configuration
| # | Location | Client/Engine | Issue | Action |
| - | -------- | ------------- | ----- | ------ |
| 1 | file:line | `create_async_engine()` | No `pool_size` | Add `pool_size=5, max_overflow=10` |
### Expensive Operations in Hot Paths
| # | Location | Operation | Frequency | Action |
| - | -------- | --------- | --------- | ------ |
| 1 | file:line | `re.compile()` in loop | Per-request | Hoist to module level |
## Recommendations (Priority Order)
1. **Must-fix**: {blocking I/O in async, N+1 in critical paths, unclosed resources, unbounded caches in production}
2. **Should-fix**: {sequential awaits, missing pool configuration, eager materialization of large datasets}
3. **Consider**: {hot path optimizations, generator conversions for moderate datasets}
file/path.py:line with the exact code.list comprehension vs generator
for small bounded datasets.development
Transforms vague feature ideas into precise, codebase-grounded technical requirements. Use when requirements are ambiguous/incomplete, the user struggles to describe behavior, terminology is unclear, or multiple concepts are mixed. Output is a requirements spec—NOT an implementation plan.
tools
Audit TypeScript type definitions for design debt — duplicated shapes, missing derivations, over-engineered generics, under-constrained type parameters, reinvented utility types, and disorganized type architecture. Type structure and maintainability, not type enforcement. Use when: reviewing type definitions for maintainability, reducing type duplication, simplifying over-engineered type-level logic, or reorganizing type architecture after growth.
development
Audit TypeScript test code for quality gaps — missing coverage on critical paths, brittle tests coupled to implementation, over-mocking, assertion-free tests, missing edge cases, and duplicated test setup. Focuses on test effectiveness, not production code structure. Use when: reviewing TypeScript test suites for reliability, reducing false-positive test failures, improving coverage of critical business logic, or cleaning up test debt.
tools
Audit TypeScript class and interface design for SOLID violations — god classes, rigid extension points, broken substitutability, fat interfaces, and concrete dependency chains. Focuses on responsibility assignment and abstraction fitness. Use when: reviewing class hierarchies, preparing for extension with new variants, reducing coupling between services, or improving testability of class-heavy code.