5 pitfalls to avoid when diving into async Python

,

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.

TaskSynchronous CodeAsync Equivalent
File I/Owith open("file.txt") as f:
  data = f.read()
async with aiofiles.open("file.txt") as f:
  data = await f.read()
Context Managerwith resource:async with resource:
Looping over thingsfor item in iterable:async for item in async_iterable:
Type hinting a coroutinedef fetch() -> strasync 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 awaits. You’ve started avoiding time.sleep(). You’re even feeling pretty confident about your async structure.

But you’re still using:

  • requests for HTTP calls
  • psycopg2 for PostgreSQL
  • boto3 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:

CategoryBlocking LibraryAsync AlternativeNotes
HTTP ClientsrequestsaiohttpFully async client/server, supports sessions, streaming, websockets.
PostgreSQL DBpsycopg2asyncpg + databasesasyncpg is fast; databases gives query helpers without full ORM.
MySQL DBpymysql, mysqlclientaiomysqlWorks similarly to asyncpg. Often used with databases.
SQLitesqlite3aiosqliteGreat for light async use, file-based DBs.
Redisredis-pyaioredis or redis.asyncioredis-py now includes async via redis.asyncio.
AWS SDKboto3aioboto3A thin async wrapper over boto3 — requires some care with sessions.
File I/Oopen(), read(), write()aiofilesSimple async wrapper around Python file ops.
S3 Direct Uploadboto3.upload_fileaiobotocore or aioboto3Use with caution — async I/O isn’t always faster for big files.
Kafkaconfluent-kafka, kafka-pythonaiokafkaAsync client for Apache Kafka, integrates well with asyncio apps.
SMTP / Emailsmtplib, emailaiosmtplibAsync SMTP client for sending email.
Elasticsearchelasticsearch-pyaioes, elasticsearch-py-asyncFewer maintained options; check version support carefully.
Task Queuescelerydramatiq, huey, arqarq 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:
    Use async def only if your test really needs to await. Otherwise, keep it def.
  • Controlled event loops:
    Don’t let your tests create messy background tasks that outlive the test scope. Use loop-scoped fixtures or tools like anyio for better isolation.
  • Mocking async code properly:
    If you use unittest.mock, your AsyncMock 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

Your email address will not be published. Required fields are marked *