python-vibe-guard

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
$ 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)
Empirical foundation
250
Repositories scanned
to validate each rule
95,678
Python files
in the sweep dataset
7,106
Findings across
all 20 rules
20
Evidence-backed rules,
none invented
189
Tests in
the test suite
100%
Rules with precision
audited rule-by-rule
Evidence example — PYVIBE-001
time.sleep() inside async def
Evidence Level A+ · the strongest classification in our methodology
# AI-generated async handler async def process_order(order_id: int): time.sleep(2) # PYVIBE-001 return order_id
31 / 250 repositories
Found in 12.4% of real codebases — 87 total hits
  • Python documentation names time.sleep() explicitly as the canonical event loop blocking call
  • Production incident: home-assistant/core#119628 — blocking sleep inside event loop degraded responsiveness in HA 2024.6.2
  • Community consensus: no known case where time.sleep() directly in async def is the correct choice over await asyncio.sleep()
  • 0 false positives documented across 87 hits in 250 repositories
Precision100%
EvidenceA+
What we actually check
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
The most important section
We document our mistakes.
PYVIBE-019 (retry without backoff) detects a real production problem — tight retry loops under I/O failure cause cascading load. It is also a rule we got badly wrong, measured, redesigned, and got less wrong. Every step is in the research directory.
Initial state
First detector shipped. Precision: ~12%
Broad heuristic: any while True loop without sleep inside async def. 760 hits across 250 repos. FP rate: ~88%. Almost none of them were actual retry bugs.
Measured, not rationalized
Full precision audit: every hit manually classified.
Three FP categories identified that cannot be resolved with pure AST analysis: BENCHMARK_LOOP (performance benchmarks iterating N times), TRY_ALTERNATIVES (trying N different strategies, not retrying one thing), GRAPH_TRAVERSAL (iterating nodes, not I/O retries). While loops excluded entirely — irreducible FP rate of ~90% with AST alone.
Redesigned — Scan v4 (June 2026)
Scope restricted to for-loop retries only. Precision: 67%
New scope: 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.
Current status: 🔵 Limited Scope

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.

Installation
$ pip install python-vibe-guard # Run on your project $ python -m pyvibe your_module/ # Or use the CLI entrypoint directly $ pyvibe your_module/
Requires Python 3.10+. No external runtime dependencies.
# .pre-commit-config.yaml repos: - repo: https://github.com/Joaquinriosheredia/python-vibe-guard rev: HEAD hooks: - id: python-vibe-guard
Get early access

PyPI packaging, CI integrations, and java-vibe-guard are in progress. Leave your email if you want to know when those land.