Python Async Antipattern Detector — Find blocking calls, event loop misuse, and concurrency bugs in async code. Zero dependencies.
| Tool | Deps | Blocking Calls | Event Loop | Concurrency | Shared State |
|---|---|---|---|---|---|
| asyncaudit | 0 | ✅ | ✅ | ✅ | ✅ |
| flake8-async | flake8 | ✅ | ❌ | ❌ | ❌ |
| pylint-blocking-calls | pylint | ✅ | ❌ | ❌ | ❌ |
| ruff (ASYNC rules) | ruff | partial | ❌ | ❌ | ❌ |
# Check a project
python asyncaudit.py src/
# With fix suggestions
python asyncaudit.py --verbose src/
# CI mode
python asyncaudit.py --check --threshold 80 src/
# JSON output
python asyncaudit.py --json src/Blocking Calls (8 rules)
| Rule | Sev | Pattern |
|---|---|---|
| A01 | 🔴 | time.sleep() in async (blocks event loop) |
| A02 | 🔴 | Sync file I/O: open(), os.read(), pathlib.Path.read_text() |
| A03 | 🔴 | Sync HTTP: requests.get(), urllib.request.urlopen() |
| A04 | 🔴 | subprocess.run() / os.system() |
| A05 | input() blocks event loop |
|
| A06 | threading.Lock/Semaphore (use asyncio.Lock) |
|
| A07 | queue.Queue (use asyncio.Queue) |
|
| A08 | Sync DB: sqlite3.connect, psycopg2.connect |
Event Loop (3 rules)
| Rule | Sev | Pattern |
|---|---|---|
| A20 | 🔴 | asyncio.run() inside async function (RuntimeError) |
| A21 | asyncio.get_event_loop() deprecated |
|
| A22 | ℹ️ | Manual event loop management |
Concurrency (4 rules)
| Rule | Sev | Pattern |
|---|---|---|
| A30 | Sequential awaits (should use gather()) |
|
| A31 | Unbounded gather() (add semaphore) |
|
| A32 | ℹ️ | Missing timeout on external calls |
| A33 | Fire-and-forget create_task() (task may be GC'd) |
Thread/State Safety (2 rules)
| Rule | Sev | Pattern |
|---|---|---|
| A40 | ℹ️ | Creating threads inside async functions |
| A41 | Mutating module-level state without asyncio.Lock |
70+ blocking calls detected including requests, urllib, subprocess, os.system, sqlite3, psycopg2, pymysql, pymongo, redis, pathlib, shutil, smtplib, and more.
# ❌ Bad: blocks the event loop
async def fetch_data(url):
time.sleep(1) # A01: use await asyncio.sleep()
response = requests.get(url) # A03: use aiohttp/httpx
with open("cache.json") as f: # A02: use aiofiles
cache = json.load(f)
return response.json()
# ✅ Good: non-blocking
async def fetch_data(url):
await asyncio.sleep(1)
async with aiohttp.ClientSession() as session:
async with session.get(url) as response:
return await response.json()| Flag | Description |
|---|---|
--check |
Exit with code 1 if score below threshold |
--threshold N |
Score threshold (default: 80) |
--json |
Output as JSON |
--severity LEVEL |
Min: error, warning, info |
--ignore RULES |
Skip rules (e.g., A02,A05) |
--verbose |
Show fix suggestions |
--quiet |
Only show summary |
--list-rules |
List all rules |
- Python 3.9+
- Zero dependencies
MIT