How to Create Event Loop in Python: Complete Guide with Examples
Last verified: April 2026
Executive Summary
Event loops are the backbone of Python’s asynchronous programming model, yet most developers create them without fully understanding the underlying mechanics. The primary method involves using asyncio.new_event_loop() or asyncio.run(), with the latter being the modern, recommended approach introduced in Python 3.7. Our analysis shows that improper event loop management leads to resource leaks, hanging tasks, and performance degradation in production environments.
Learn Python on Udemy
Creating an event loop in Python requires understanding three critical components: loop initialization, task management, and proper cleanup. Whether you’re building a web server, handling concurrent I/O operations, or managing background tasks, knowing how to correctly instantiate and configure an event loop directly impacts application stability and performance. This guide covers the standard library approach, common pitfalls, and production-ready patterns you need to implement robust async code.
Main Data Table: Event Loop Creation Methods
| Method | Python Version | Use Case | Manual Cleanup |
|---|---|---|---|
asyncio.run() |
3.7+ | Main entry point, script execution | No (automatic) |
asyncio.new_event_loop() |
3.5+ | Fine-grained control, advanced scenarios | Yes (required) |
asyncio.get_event_loop() |
3.5+ | Get running loop (legacy, deprecated) | Varies |
asyncio.get_running_loop() |
3.7+ | Inside async context, thread-safe | No |
Breakdown by Experience Level
Beginner developers should start with asyncio.run(), which handles loop creation, task execution, and cleanup automatically. This single-function approach eliminates the most common mistake: forgetting to close the event loop.
Intermediate developers typically need asyncio.new_event_loop() when integrating async code with existing synchronous frameworks or managing multiple concurrent operations with shared state.
Advanced developers work with custom event loop policies, platform-specific loop implementations (like uvloop for performance), and complex resource management patterns.
Comparison Section: Event Loop Creation Methods
| Approach | Complexity | Automation | Control | Recommended |
|---|---|---|---|---|
| asyncio.run() | Low | High | Limited | ✓ Default choice |
| new_event_loop() + set_event_loop() | Medium | Low | High | ✓ Complex scenarios |
| get_event_loop() | Low | None | None | ✗ Legacy only |
| Third-party: uvloop | Medium | Medium | High | ✓ Performance-critical |
Key Factors Affecting Event Loop Creation
1. Python Version Compatibility
asyncio.run() was introduced in Python 3.7 and should be your default choice for new projects. If you’re maintaining code for Python 3.6 or earlier, use asyncio.new_event_loop() and asyncio.set_event_loop(). The version difference matters because older approaches had inconsistent behavior across platforms and threading scenarios.
2. Resource Cleanup Requirements
One of the most common mistakes in production code is leaving event loops open. When you use asyncio.new_event_loop(), you must explicitly call loop.close(). Forgetting this causes file descriptor leaks and prevents proper garbage collection. Using asyncio.run() eliminates this concern entirely—it automatically closes the loop when the coroutine completes, even if exceptions occur.
3. Thread Safety and Execution Context
Event loops are not thread-safe by design. Each thread needs its own event loop. When creating multiple loops or working in multi-threaded applications, use asyncio.new_event_loop() and call asyncio.set_event_loop() within each thread. Inside an already-running async context, use asyncio.get_running_loop(), which raises an error if no loop is active—a helpful safety feature that prevents silent failures.
4. Integration with Synchronous Code
When bridging async and sync code, you need explicit loop management. Libraries like Flask or Django require creating a loop, running it within an executor, or using adapters. The pattern involves creating the loop with asyncio.new_event_loop(), setting it as the thread’s current loop, and running your async tasks via loop.run_until_complete().
5. Platform-Specific Event Loop Policies
Windows and Unix-based systems have different default loop implementations. Windows defaults to ProactorEventLoop (supports all operations), while Unix prefers SelectorEventLoop (more efficient for socket operations). You can override this behavior with asyncio.set_event_loop_policy(), which matters for cross-platform applications handling pipes, subprocesses, or specific I/O patterns.
Historical Trends
Python’s async ecosystem has evolved significantly. In Python 3.5-3.6, developers had to manually manage event loops using asyncio.get_event_loop() and loop.run_until_complete(). This approach was error-prone because the behavior differed depending on whether code ran in the main thread, a background thread, or an interactive environment.
Python 3.7 introduced asyncio.run(), which standardized the entry point for async programs. This single change reduced the most common category of bugs: improperly closed event loops and dangling tasks. The Python core team has continued improving asyncio with each release, adding asyncio.Runner (3.11+) for even finer control and deprecating problematic patterns like calling get_event_loop() outside of async contexts.
For high-performance applications, uvloop emerged as a drop-in replacement offering 2-4x faster I/O performance by implementing the event loop in Cython. Modern frameworks like FastAPI recommend uvloop for production deployments handling thousands of concurrent connections.
Expert Tips
Tip 1: Always Use asyncio.run() as Your Default Pattern
Start every async application with asyncio.run(). It’s the clearest, safest approach that handles initialization and cleanup automatically. Only deviate if you have specific requirements for multiple loops, explicit loop configuration, or integration with existing frameworks.
Tip 2: Wrap Edge Cases and Handle Resource Leaks
Even with asyncio.run(), always handle exceptions in your coroutines. Unhandled exceptions in tasks can be silently suppressed. Use try/except blocks, implement proper cleanup in finally blocks, and set up exception handlers with loop.set_exception_handler() for debugging background tasks.
Tip 3: Use Context Managers for Loop Lifecycle Management
When creating loops manually, wrap them in context managers to guarantee cleanup. Here’s the pattern:
import asyncio
from contextlib import asynccontextmanager
@asynccontextmanager
async def managed_loop():
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
try:
yield loop
finally:
loop.close()
Tip 4: Monitor for Common Pitfalls in Your Code
Check for these errors: calling loop.run_until_complete() on an already-running loop (raises RuntimeError), forgetting await in async functions (task silently completes), not setting loop explicitly in threads (causes “no running event loop” errors), and leaving TCP/UDP sockets open within async tasks (leads to resource exhaustion).
Tip 5: Consider uvloop for Production Performance
For applications handling 1,000+ concurrent connections, test with uvloop. It’s a drop-in replacement requiring only two extra lines:
import asyncio
import uvloop
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
asyncio.run(main())
FAQ Section
What’s the difference between asyncio.run() and asyncio.new_event_loop()?
asyncio.run() is a high-level convenience function that creates a new event loop, runs your coroutine, and closes the loop—all in one call. It’s recommended for most applications. asyncio.new_event_loop() gives you explicit control: you create the loop, configure it, run tasks with loop.run_until_complete(), and must manually call loop.close(). Use new_event_loop() when you need to manage multiple loops, run multiple coroutines sequentially with state preservation, or integrate with frameworks that require explicit loop management.
Do I need to call loop.close() after using asyncio.run()?
No. asyncio.run() automatically closes the event loop when your coroutine completes, even if exceptions are raised. This is one of its main advantages over manual loop management. If you use asyncio.new_event_loop(), you must explicitly call loop.close() or use a context manager to ensure cleanup happens.
Can I create multiple event loops in the same program?
Yes, but only one loop can run at a time per thread. You can create multiple loops with asyncio.new_event_loop() and switch between them using asyncio.set_event_loop(loop), but this is rarely necessary. A more common pattern is running different event loops in different threads. Each thread must have its own loop, and you coordinate between them using thread-safe primitives like Queue or Lock.
What happens if I call asyncio.get_event_loop() in Python 3.10+?
In Python 3.10+, calling asyncio.get_event_loop() outside of an async context or the main thread will raise a RuntimeError. This deprecation is intentional: the function was a major source of confusion and bugs. Instead, use asyncio.run() for entry points or asyncio.get_running_loop() inside async code. If you’re integrating with legacy code that expects a default loop, explicitly create and set one with asyncio.set_event_loop(asyncio.new_event_loop()).
How do I handle event loop errors in production?
Set a custom exception handler to log unexpected errors in background tasks:
def exception_handler(loop, context):
exception = context.get('exception')
if isinstance(exception, asyncio.CancelledError):
return # Expected for cancellation
logger.error(f"Event loop error: {context['message']}", exc_info=exception)
loop = asyncio.new_event_loop()
loop.set_exception_handler(exception_handler)
asyncio.set_event_loop(loop)
Conclusion
Creating an event loop in Python comes down to choosing the right tool for your situation. For nearly all new projects, use asyncio.run()—it’s simple, safe, and follows Python best practices. The automatic cleanup alone prevents a class of production bugs that plague code using older patterns.
When you need more control, reach for asyncio.new_event_loop() with proper resource management. Always wrap cleanup in context managers or finally blocks. Watch for the common mistakes listed in this guide: ignoring edge cases, forgetting to close loops, assuming thread safety, and leaving resources open within async tasks.
Remember that event loop creation is just the beginning. The real complexity lies in managing concurrent tasks, handling errors gracefully, and ensuring resource cleanup. For production systems handling high concurrency, invest time in testing your async code thoroughly and consider performance-optimized alternatives like uvloop. Start simple with asyncio.run(), measure performance, and only add complexity when data shows you need it.
Learn Python on Udemy
Related tool: Try our free calculator