Production bugs generated by AI don't look like syntax errors.
They pass CI. They pass tests. They fail under load.
python-vibe-guard finds them before production does.
$ python3 -m pyvibe demo/bad_async.py python-vibe-guard ───────────────────────────────────────────── demo/bad_async.py [CRITICAL] [PYVIBE-001] — line 27 Function : process_order() Problem : time.sleep() blocks the entire event loop Fix : Use `await asyncio.sleep(n)` instead [CRITICAL] [PYVIBE-002] — line 33 Function : fetch_user() Problem : requests.get() is synchronous — blocks the event loop Fix : Use `async with httpx.AsyncClient() as c: await c.get(url)` [CRITICAL] [PYVIBE-003] — line 39 Function : orchestrate() Problem : asyncio.run() inside async def raises RuntimeError at runtime Fix : Use `await coroutine()` directly — asyncio.run() is for sync entrypoints only [CRITICAL] [PYVIBE-004] — line 45 Function : update_counter() Problem : threading.Lock() blocks the event loop under contention Fix : Use `asyncio.Lock()` with `async with lock:` instead [CRITICAL] [PYVIBE-005] — line 52 Function : process_payment() Problem : Celery task defined without soft_time_limit or time_limit — can hang forever Fix : Use @app.task(soft_time_limit=30, time_limit=60) to bound execution time [CRITICAL] [PYVIBE-006] — line 58 Function : handle_request() Problem : ContextVar.set() without guaranteed cleanup leaks state between async tasks Fix : Capture the token: `token = var.set(v)` then call `var.reset(token)` in a finally block [CRITICAL] [PYVIBE-007] — line 64 Function : transcode_video() Problem : subprocess.run() blocks the event loop for the entire subprocess duration Fix : Use `proc = await asyncio.create_subprocess_exec(*cmd)` then `await proc.communicate()` [CRITICAL] [PYVIBE-008] — line 69 Function : get_users() Problem : sqlite3.connect() is synchronous — blocks the event loop during I/O Fix : Use `async with aiosqlite.connect('db.sqlite3') as db:` instead [CRITICAL] [PYVIBE-009] — line 75 Function : read_config() Problem : open() performs synchronous file I/O — blocks the event loop Fix : Use `async with aiofiles.open(path) as f: content = await f.read()` [CRITICAL] [PYVIBE-010] — line 81 Function : fetch_data() Problem : httpx.get() is synchronous — blocks the event loop for the full HTTP round-trip Fix : Use `async with httpx.AsyncClient() as client: await client.get(url)` [CRITICAL] [PYVIBE-011] — line 89 Function : run_script() Problem : os.system() blocks the OS thread — no direct async equivalent Fix : Use `proc = await asyncio.create_subprocess_shell(cmd)` then `await proc.communicate()` [CRITICAL] [PYVIBE-012] — line 94 Function : notify_user() Problem : asyncio.create_task() return value discarded — task may be GC'd and silently cancelled Fix : Assign and await: `task = asyncio.create_task(coro()); await task` [CRITICAL] [PYVIBE-013] — line 99 Function : fetch_all() Problem : asyncio.gather() without return_exceptions=True — first exception leaks remaining tasks Fix : Add return_exceptions=True and check results [CRITICAL] [PYVIBE-014] — line 105 Function : notify_legacy() Problem : asyncio.ensure_future() return value discarded — task may be GC'd and silently cancelled Fix : Assign and await: `task = asyncio.ensure_future(coro()); await task` [CRITICAL] [PYVIBE-015] — line 111 Function : bridge_sync() Problem : loop.run_until_complete() inside async def — raises RuntimeError at runtime (event loop already running) Fix : Replace with `await coro()` directly; the enclosing async function is already on the event loop [CRITICAL] [PYVIBE-016] — line 116 Function : fetch_sync_client() Problem : httpx.Client() is synchronous — blocks the event loop for every HTTP request Fix : Use `async with httpx.AsyncClient() as client: response = await client.get(url)` instead [WARNING] [PYVIBE-017] — line 124 Function : process_order_silently() Problem : except Exception with empty body silences all errors silently Fix : Log the error: `except Exception as e: logger.error(e)`, or re-raise: `except Exception: raise` [CRITICAL] [PYVIBE-018] — line 130 Function : background_worker() Problem : while True loop inside async def has no await — event loop blocked indefinitely Fix : Add `await asyncio.sleep(0)` to yield control, or `await asyncio.sleep(N)` for polling [WARNING] [PYVIBE-019] — line 139 Function : retry_upload() Problem : retry loop without backoff — except retries immediately with no delay Fix : Add `await asyncio.sleep(2 ** attempt)` before continue, or use tenacity / backoff library [WARNING] [PYVIBE-020] — line 145 Function : event_producer() Problem : put_nowait() without asyncio.QueueFull handler — raises silently if queue is full, item is lost Fix : Wrap in `try/except asyncio.QueueFull:` and log, or use `await queue.put()` to block until space ───────────────────────────────────────────── 20 violation(s) in 1 file(s)
time.sleep() explicitly as the canonical event loop blocking call
time.sleep() directly in async def is the correct choice over await asyncio.sleep()
| Most linters | python-vibe-guard | |
|---|---|---|
| Basis for a rule | opinions · style · conventions · syntax | empirical validation against 250 real repositories |
| Precision | not measured | audited rule-by-rule, results published in research/ |
| False positives | undocumented | documented per rule with named categories |
| Limitations | rarely stated | explicitly stated — some rules carry a Limited Scope marker |
| Evidence | conventions · style guides | production incidents · official docs · community consensus |
| Methodology | internal | public — research/ directory in the repository |
while True loop without sleep inside async def.
760 hits across 250 repos. FP rate: ~88%.
Almost none of them were actual retry bugs.
for _ in range(N) / for attempt in range(N) in async def
with except: continue and no sleep or backoff in the except handler.
18 hits audited at 100%. Result: 12 TP · 6 FP · 67% precision.
FP rate 33% — below the 40% threshold for heuristic rules in our methodology.
PYVIBE-019 ships with WARNING severity and a visible Limited Scope marker. The 3 FP categories are documented with their signatures so a reader can check whether their hit matches a known false positive before acting on it. A 67%-precise rule that tells you what it doesn't know is more useful than a 95%-precise rule that pretends it never misses.
PyPI packaging, CI integrations, and java-vibe-guard are in progress. Leave your email if you want to know when those land.