Async I/O and concurrency: Python for developers coming from goroutines
Learn Python's async I/O model and concurrency tradeoffs without assuming goroutines and channels transfer directly.
by the end of this lesson you can
- →Chooses async only if concurrent I/O is actually useful
- →Avoids pretending goroutines map directly onto Python
- →Keeps the resulting design readable in Python terms
Overview
Go makes concurrency feel like an everyday structural tool. Python can handle concurrent work too, but the model is different. asyncio is mainly about I/O concurrency, not a drop-in equivalent to goroutines, and many Python programs stay synchronous until concurrency is clearly worth the complexity.
In Go, you often
launch goroutines easily, coordinate with channels, and treat concurrency as a routine part of service and tooling code.
In Python, the common pattern is
to use asyncio for concurrent I/O, threads or processes for specific cases, and otherwise keep code synchronous when that is clearer.
why this difference matters
This is the most pair-specific advanced lesson because Go developers are especially likely to expect Python concurrency to feel lighter and more universal than it does in practice.
Go
go fetchUser(id)Python
user = await fetch_user(user_id)Deeper comparison
Go version
userCh := make(chan User)
postCh := make(chan []Post)
go func() { userCh <- fetchUser(id) }()
go func() { postCh <- fetchPosts(id) }()
user := <-userCh
posts := <-postChPython version
user, posts = await asyncio.gather(
fetch_user(user_id),
fetch_posts(user_id),
)Reflect
When should a Go developer resist adding concurrency to Python code, and when is asyncio genuinely the right fit?
what a strong answer notices
A strong answer mentions I/O-bound coordination, the cost of added complexity, and the fact that Python often benefits from staying synchronous until concurrent work is clearly central.
Rewrite
Translate this Go concurrent shape into Python and choose between plain synchronous code and async I/O intentionally.
Rewrite this Go
results := make(chan Result)
go func() {
results <- loadRemote()
}()
value := <-resultswhat good looks like
- Chooses async only if concurrent I/O is actually useful
- Avoids pretending goroutines map directly onto Python
- Keeps the resulting design readable in Python terms
Practice
Design a Python function that fetches config and user data from two remote services, and explain when you would keep it synchronous instead of using asyncio.
success criteria
- Uses async I/O for a clear reason
- Explains the tradeoff against simpler synchronous code
- Does not assume Go-style concurrency ergonomics
Common mistakes
- Expecting asyncio to feel like goroutines with different syntax.
- Adding concurrency to CPU-light or low-latency code that was clearer synchronously.
- Looking for channel-shaped solutions before understanding Python's usual I/O patterns.
takeaways
- ●This is the most pair-specific advanced lesson because Go developers are especially likely to expect Python concurrency to feel lighter and more universal than it does in practice.
- ●A strong answer mentions I/O-bound coordination, the cost of added complexity, and the fact that Python often benefits from staying synchronous until concurrent work is clearly central.
- ●Uses async I/O for a clear reason