Deep Analysis of asyncio.run Missing Issue in Python 3.6 and Asynchronous Programming Practices

Dec 05, 2025 · Programming · 13 views · 7.8

Keywords: Python Asynchronous Programming | asyncio.run | Python Version Compatibility | Event Loop | Coroutine

Abstract: This article provides an in-depth exploration of the AttributeError issue caused by the absence of asyncio.run in Python 3.6. By analyzing the core mechanisms of asynchronous programming, it explains the introduction background of asyncio.run in Python 3.7 and its alternatives in Python 3.6. Key topics include manual event loop management, comparative usage of asyncio.wait and asyncio.gather, and writing version-compatible asynchronous code. Complete code examples and best practice recommendations are provided to help developers deeply understand the evolution and practical applications of Python asynchronous programming.

Problem Background and Error Analysis

In the learning process of Python asynchronous programming, many developers encounter error messages similar to the following:

Traceback (most recent call last):
  File "test.py", line 15, in <module>
    asyncio.run(asyncio.wait(futures))
AttributeError: module 'asyncio' has no attribute 'run'
sys:1: RuntimeWarning: coroutine 'call_url' was never awaited

The core issue is that the asyncio.run() function was introduced as a new feature in Python 3.7. In Python 3.6 and earlier versions, asynchronous programs need to be executed through manual event loop management.

Version Evolution of Python Asynchronous Programming

Python's asyncio module has undergone significant API improvements since its introduction in version 3.4. Before Python 3.7, starting asynchronous programs required explicit acquisition and management of event loops:

import asyncio

# Create coroutine task list
async def task():
    await asyncio.sleep(1)
    return "Completed"

# Writing for Python 3.6 and earlier
loop = asyncio.get_event_loop()
try:
    result = loop.run_until_complete(task())
    print(f"Result: {result}")
finally:
    loop.close()

While this manual management approach offers flexibility, it increases code complexity and error probability. The introduction of asyncio.run() aims to simplify this process by automatically handling event loop creation, execution, and cleanup.

Solution for Python 3.6

For the code in the original problem, modification is required in Python 3.6 environment:

import asyncio
import aiohttp

urls = ['http://www.google.com', 'http://www.yandex.ru', 'http://www.python.org']

async def call_url(url):
    print(f'Starting request for {url}')
    async with aiohttp.ClientSession() as session:
        async with session.get(url) as response:
            data = await response.text()
            print(f'{url}: {len(data)} bytes')
            return data

async def main():
    tasks = [call_url(url) for url in urls]
    # Use asyncio.gather to collect all results
    results = await asyncio.gather(*tasks, return_exceptions=True)
    for url, result in zip(urls, results):
        if isinstance(result, Exception):
            print(f"{url} request failed: {result}")
        else:
            print(f"{url} request successful")

# Python 3.6 compatible startup method
if __name__ == "__main__":
    loop = asyncio.get_event_loop()
    try:
        loop.run_until_complete(main())
    finally:
        loop.close()

This improved version not only solves version compatibility issues but also incorporates best practices for error handling and resource management.

Comparison of asyncio.wait and asyncio.gather

In asynchronous programming, both asyncio.wait() and asyncio.gather() are commonly used task scheduling functions, but they differ in semantics and usage scenarios:

import asyncio

async def worker(name, delay):
    print(f"{name} starting work")
    await asyncio.sleep(delay)
    print(f"{name} completed work")
    return f"{name}_result"

async def compare_methods():
    tasks = [worker(f"Task{i}", i) for i in range(1, 4)]
    
    # Using asyncio.wait
    print("Using asyncio.wait:")
    done, pending = await asyncio.wait(tasks, timeout=2.5)
    for task in done:
        print(f"Completed: {task.result()}")
    
    # Using asyncio.gather
    print("\nUsing asyncio.gather:")
    results = await asyncio.gather(*tasks, return_exceptions=True)
    for result in results:
        print(f"Result: {result}")

# Execute comparison
loop = asyncio.get_event_loop()
loop.run_until_complete(compare_methods())

asyncio.wait() provides finer-grained control with timeout settings and returns pending tasks, while asyncio.gather() is more suitable for scenarios requiring collection of all results.

Best Practices for Version Compatibility

To write asynchronous code compatible across different Python versions, the following strategies can be adopted:

import asyncio
import sys

def run_async(coro):
    """Cross-version asynchronous execution function"""
    if sys.version_info >= (3, 7):
        # Python 3.7+ uses asyncio.run
        return asyncio.run(coro)
    else:
        # Python 3.6 and earlier versions
        loop = asyncio.get_event_loop()
        try:
            return loop.run_until_complete(coro)
        finally:
            if not loop.is_closed():
                loop.close()

async def example_coroutine():
    await asyncio.sleep(1)
    return "Asynchronous operation completed"

# Using compatible startup function
if __name__ == "__main__":
    result = run_async(example_coroutine())
    print(f"Execution result: {result}")

This approach ensures code portability across different Python versions while maintaining code clarity.

Core Concepts of Asynchronous Programming

Understanding Python asynchronous programming requires mastery of several key concepts:

  1. Coroutine: Functions defined with async def that can pause execution via await expressions
  2. Event Loop: The core scheduler of asynchronous programs, responsible for managing and executing all coroutine tasks
  3. Future Objects: Represent the eventual result of asynchronous operations, bridging coroutines and callbacks
  4. Task Objects: Wrappers for coroutines used for scheduling execution in event loops

Proper understanding of these concepts is crucial for writing efficient and reliable asynchronous programs.

Performance Optimization Recommendations

When writing asynchronous network requests, attention should be paid to the following performance optimization points:

import asyncio
import aiohttp
from datetime import datetime

async def optimized_fetch(urls, max_concurrent=10):
    """Optimized concurrent request function"""
    connector = aiohttp.TCPConnector(limit=max_concurrent)
    timeout = aiohttp.ClientTimeout(total=30)
    
    async with aiohttp.ClientSession(
        connector=connector,
        timeout=timeout
    ) as session:
        
        async def fetch_one(url):
            try:
                async with session.get(url) as response:
                    data = await response.text()
                    return url, len(data), None
            except Exception as e:
                return url, 0, str(e)
        
        tasks = [fetch_one(url) for url in urls]
        results = await asyncio.gather(*tasks)
        
        return results

# Performance testing
async def performance_test():
    urls = [f"http://httpbin.org/delay/{i}" for i in range(1, 6)]
    
    start_time = datetime.now()
    results = await optimized_fetch(urls, max_concurrent=3)
    end_time = datetime.now()
    
    print(f"Total time: {(end_time - start_time).total_seconds():.2f} seconds")
    for url, length, error in results:
        if error:
            print(f"{url}: Error - {error}")
        else:
            print(f"{url}: {length} bytes")

# Execute test
loop = asyncio.get_event_loop()
loop.run_until_complete(performance_test())

By limiting concurrent connections, setting reasonable timeout values, and reusing connection sessions, the performance and stability of asynchronous network requests can be significantly improved.

Conclusion and Future Outlook

Python asynchronous programming is a continuously evolving field. The evolution from Python 3.6 to 3.7 reflects language designers' ongoing improvements to developer experience. While the introduction of asyncio.run() simplifies the startup process of asynchronous programs, understanding its underlying principles remains essential for writing high-quality asynchronous code.

For developers still using Python 3.6, mastering manual event loop management techniques is necessary. Meanwhile, adopting version-compatible programming patterns ensures long-term code maintainability. As Python versions continue to update, developers are advised to keep up with new features while maintaining understanding of underlying mechanisms to quickly identify and solve problems when they arise.

Asynchronous programming represents not just syntactic changes but a paradigm shift in programming. By deeply understanding core concepts such as event loops, coroutines, and task scheduling, developers can write more efficient and reliable concurrent applications.

Copyright Notice: All rights in this article are reserved by the operators of DevGex. Reasonable sharing and citation are welcome; any reproduction, excerpting, or re-publication without prior permission is prohibited.