So you’ve decided to “go async.”
Maybe you were promised better performance. Maybe someone told you it would help with scale. Or maybe—just maybe—you were just tired of watching your app freeze while waiting for an API call.
Either way, welcome to the async club. We meet weekly. We cry occasionally. We post passive-aggressive tweets about asyncio stack traces.
But hey, you made it. You imported asyncio
, started sprinkling async def
into your code like seasoning, and suddenly felt like you were in control of the matrix.
Until… you weren’t.
Turns out, async isn’t just a feature — it’s a mindset. And like any mindset shift, it comes with traps. Subtle ones. Infuriating ones. Ones that make you question your career choices.
Let me walk you through five async pitfalls that personally ruined my day (or several), so maybe you can ruin yours a little less.
Welcome to the wonderful world of doing async wrong.
Here’s a list of async landmines I stepped on so you don’t have to.
Pitfall 1: Forgetting the await
— When the Linter Knows You’re Lying
You wrote an async function. You called it. It returned something. And you moved on with your life.
Until your editor quietly underlined the line with something like:
"Unexpected type: Coroutine."
And you thought, “Okay, weird. But it seems fine?”
So you test the function.
You print the result.
And… you get this:
coroutine object get_user_data at 0x10bdfc340>
You squint. You sigh. You feel a tiny bit of shame.
Because what happened is simple:
You forgot the await
.
You asked Python to run something async — and then just… didn’t wait for it. Like texting someone and closing your phone before the message sends.
In sync Python, calling a function runs it.
In async Python, calling a function prepares it to run — but nothing happens unless you await
.
It’s not just a gotcha. It’s a trap that leads to broken behavior without any errors. Your code just… doesn’t do what you think it does. And it doesn’t tell you why.
And if you’re really unlucky, that coroutine sneaks into your template or gets returned in a response, and now you’ve got a blank dashboard showing literal coroutine objects to real users.
So yeah — forget await
, and async will still run your function… just not in this reality.
Pitfall 2: Thinking Regular Python Patterns Just Work in Async Land
Let’s say you’ve jumped into the async life. You’ve read the blog posts, learned about the event loop, started sprinkling async def
and await
like salt on fries.
So far, so good.
Then you go to open a file:
with open("data.txt") as f:
return f.read()
And everything still works — no warnings, no errors. You’re feeling powerful.
Except… you just blocked the event loop.
Because here’s the catch: you’re writing async Python, but still using synchronous building blocks. And unlike a strongly typed language or a grumpy compiler, Python will let you. It doesn’t scream, it doesn’t stop you — it just quietly undermines your whole async effort.
You can’t just toss in async def
and assume the rest of your stack knows what to do. The language gives you the rope. And yes, you’ll probably hang your event loop with it.
Task | Synchronous Code | Async Equivalent |
---|---|---|
File I/O | with open("file.txt") as f: data = f.read() | async with aiofiles.open("file.txt") as f: data = await f.read() |
Context Manager | with resource: | async with resource: |
Looping over things | for item in iterable: | async for item in async_iterable: |
Type hinting a coroutine | def fetch() -> str | async def fetch() -> Coroutine[Any, Any, str] or -> Awaitable[str] |
Database (sync client) | conn = psycopg2.connect() | conn = await database.connect() (e.g. with databases + asyncpg ) |
Dependency/context cleanup | @contextmanager | @asynccontextmanager (from contextlib ) |
The issue with this one is how silent it is. Your app won’t crash. Your tests might even pass. But in production, you’ll start noticing:
- Odd pauses under load
- Inconsistent performance
- Broken concurrency, because something you thought was async is actually blocking everyone else
It’s like building a microservices architecture and realizing you hardcoded half the services into one file.
Async in Python is not a feature — it’s a mode of operation. You’re opting into a different set of tools, behaviors, and mental models. If you mix and match, you won’t get warnings — just weird bugs and poor performance.
So if you’re going async, go all in. Learn the ecosystem. Replace your with
with async with
. Migrate your I/O libraries. And don’t trust that old code snippet from Stack Overflow that “just works.”
Because yeah, it works — but not how you think.
Pitfall 3: Not Writing Async Functions…
Let me set the scene.
You’ve got your shiny new async def
, you’ve remembered the await
this time (gold star for you), and you’re feeling unstoppable. You even refactored all your views in Quart to be async because you heard it makes everything faster.
And then… you test the app.
It’s… still slow?
Or laggy?
Or worse — no different at all?
So you dig in. You check your fancy new async def handle_user_profile()
function. Looks right.
But inside, you see this:
Edituser = get_user_from_db(user_id)
pdf = generate_pdf(user)
Both of those functions? Totally, unapologetically synchronous. This is one of those moments where async quietly turns to you and says,
“Just because you called yourself async doesn’t mean you actually are.”
Because see, in Python, slapping async def
on a function doesn’t magically make your code asynchronous. The function is async — but the stuff inside? That’s still sync unless you change it.
So your async app is still waiting — blocking the event loop — every time you call a slow, synchronous function inside an async one. And unlike sync apps that can at least queue stuff properly, now you’re freezing the whole app while pretending you’re being efficient.
The worst part? You won’t get any warnings. Your code will run. Your tests might pass. But in production, it’ll feel async but act just as sluggish as before.
If anything, it might even be worse — because you now have the illusion of concurrency without any of the actual benefits.
Pitfall 4: Holding On to Synchronous Libraries Like They’re Comfort Food
Alright, so maybe you’re past the basics now.
You’ve remembered your await
s. You’ve started avoiding time.sleep()
. You’re even feeling pretty confident about your async structure.
But you’re still using:
requests
for HTTP callspsycopg2
for PostgreSQLboto3
for AWS stuff
And you’re wondering why your async app still acts like it’s stuck in line at the DMV.
Here’s the truth: if you’re building an async app, your libraries need to be async too.
Because Python doesn’t care how hip your framework is — if your code’s depending on a library that blocks the event loop, everything else has to wait.
That’s what happens when you mix blocking libraries into an async environment. You don’t just slow down that one function — you stall everything using the same event loop. Every API route. Every websocket. Every innocent user trying to load a page.
And I get it — replacing familiar libraries is work but this table could help:
Category | Blocking Library | Async Alternative | Notes |
---|---|---|---|
HTTP Clients | requests | aiohttp | Fully async client/server, supports sessions, streaming, websockets. |
PostgreSQL DB | psycopg2 | asyncpg + databases | asyncpg is fast; databases gives query helpers without full ORM. |
MySQL DB | pymysql , mysqlclient | aiomysql | Works similarly to asyncpg . Often used with databases . |
SQLite | sqlite3 | aiosqlite | Great for light async use, file-based DBs. |
Redis | redis-py | aioredis or redis.asyncio | redis-py now includes async via redis.asyncio . |
AWS SDK | boto3 | aioboto3 | A thin async wrapper over boto3 — requires some care with sessions. |
File I/O | open() , read() , write() | aiofiles | Simple async wrapper around Python file ops. |
S3 Direct Upload | boto3.upload_file | aiobotocore or aioboto3 | Use with caution — async I/O isn’t always faster for big files. |
Kafka | confluent-kafka , kafka-python | aiokafka | Async client for Apache Kafka, integrates well with asyncio apps. |
SMTP / Email | smtplib , email | aiosmtplib | Async SMTP client for sending email. |
Elasticsearch | elasticsearch-py | aioes , elasticsearch-py-async | Fewer maintained options; check version support carefully. |
Task Queues | celery | dramatiq , huey , arq | arq and dramatiq support asyncio natively. Celery needs workarounds. |
Pitfall 5: Forgetting to Set Up Async in Your Tests Properly
Async in production is one thing.
Async in tests? That’s where the chaos really lives.
You start writing unit tests for your async code like this:
def test_fetch_user():
result = fetch_user()
assert result.name == "Alice"
Then Python slaps back with something like:
RuntimeWarning: coroutine 'fetch_user' was never awaited
So you think, fine — I’ll make it async:
async def test_fetch_user():
result = await fetch_user()
assert result.name == "Alice"
But now your test runner throws a fit, because it’s not designed to await test functions by default.
And thus begins your journey through the rabbit hole of pytest-asyncio
, asynctest
, anyio
, or custom event loop fixtures.
To test async code properly, you need:
- An async-compatible test runner:
pytest
+pytest-asyncio
is the go-to combo. - Correct test signatures:
Useasync def
only if your test really needs toawait
. Otherwise, keep itdef
. - Controlled event loops:
Don’t let your tests create messy background tasks that outlive the test scope. Use loop-scoped fixtures or tools likeanyio
for better isolation. - Mocking async code properly:
If you useunittest.mock
, yourAsyncMock
game better be strong. Otherwise, your mocks will say they’re async but behave like confused interns.
In Conclusion: Async Is a Superpower… If You Don’t Sabotage It
Async in Python is powerful. It’s elegant. It makes your server handle thousands of requests without breaking a sweat — until you block the event loop with a file read and your app dies in production on a Tuesday.
The truth is: async isn’t hard, but it’s easy to get wrong. Because Python doesn’t yell at you. It doesn’t mind if you open files the old way, forget await
, or return coroutine objects to unsuspecting callers. It just… lets you dig the hole. Silently. With a latte in hand.
But once you understand the traps — mixing sync and async, misusing context managers, trusting blocking libraries, or misconfiguring your tests — you start to see the shape of the async ecosystem. It’s not just a style. It’s a commitment.
When done right, async can turn your app into a performance beast.
When done wrong, async will betray you harder than eval()
in user input.
So go async — but go intentionally. Learn the patterns. Use the right tools.
And whatever you do… don’t trust code that “just works.”
Leave a Reply