<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
<channel>
<title>Coder Legion - Recent posts tagged asyncio</title>
<link>https://coderlegion.com/tag/asyncio</link>
<description>Powered by Question2Answer</description>
<item>
<title>Orchestrating Long-Running, Interactive Subprocesses with Python Asyncio</title>
<link>https://coderlegion.com/23083/orchestrating-long-running-interactive-subprocesses-with-python-asyncio</link>
<description>&lt;p&gt;Realizing our perception of time can change drastically when we are drowning in work is a rather funny experience. Reading the publishing date of the last article nearly gave me a good shock when I realized it was done half a year ago. Yet, it felt recent, and the follow-up to the project is still sitting in the filesystem awaiting planning and drafting.&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://python.plainenglish.io/actor-models-in-pygame-lessons-in-serialization-and-state-isolation-979c5af57cc5&quot;&gt;Actor Models in Pygame: Lessons in Serialization and State Isolation&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn-images-1.medium.com/max/1024/1*R74ZBNUAXxeERoONFiK3NA.png&quot; alt=&quot;&quot;&gt;&lt;br&gt;
&lt;em&gt;Cute illustration generated by Gemini&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Unfortunately, that story has to wait for a bit, as we are side-tracking to a different territory today. Definitely not a reflection of my current job situation where I am struggling outside of my comfort zone. Alrighty, let’s move on from this bad self-deprecating joke. Instead, we are abruptly revisiting Python subprocess management with AsyncIO as if nothing happened.&lt;/p&gt;
&lt;h4 id=&quot;the-limits-of-shell-scripting&quot;&gt;The Limits of Shell Scripting&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn-images-1.medium.com/max/1024/0*Lgh1QsIwhuoHBCQu&quot; alt=&quot;&quot;&gt;&lt;br&gt;
&lt;em&gt;Photo by Charles Asselin on Unsplash&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;There are times where we utilize shell scripting to automate some tasks, but to what extent should we stick with just a scripting language? With LLM getting so much more capable these days, does the choice of tool still matter?&lt;/p&gt;
&lt;p&gt;Our last attempt was about &lt;a rel=&quot;nofollow&quot; href=&quot;https://kitfucoda.medium.com/video-data-io-through-ffmpeg-subprocess-c5f1ee42e43d&quot;&gt;passing data from one utility to another&lt;/a&gt;, from a video encoder to a streamer (both ffmpeg executed with different arguments). There’s no data to be passed between processes this time, and we are just dealing with one process at a time. Sounds like a trivial shell script to write, an LLM chatbot would be more than enough for the job, that’s what I thought.&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://kitfucoda.medium.com/video-data-io-through-ffmpeg-subprocess-c5f1ee42e43d&quot;&gt;Video data IO through ffmpeg subprocess&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Fortunately, Gemini flash did deliver.&lt;/p&gt;
&lt;p&gt;Some additional background to the problem is likely warranted before we dive deeper. It is indeed true that we are not passing data between processes, but the process we are interested in managing does require some user interaction, both directly and indirectly through the command line interface. Another annoyance is that it would just silently terminate on failure and that affects my well-being.&lt;/p&gt;
&lt;p&gt;Because when it terminates, I allow my partner to punch me in the face.&lt;/p&gt;
&lt;p&gt;It was clearly a joke.&lt;/p&gt;
&lt;p&gt;But you get the point.&lt;/p&gt;
&lt;p&gt;Getting the utility in question to re-run on failure is then Gemini’s first attempt to turn the work into a shell script written in bash.&lt;/p&gt;
&lt;h4 id=&quot;modern-prototyping-with-ai-autocomplete&quot;&gt;Modern Prototyping with AI Autocomplete&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn-images-1.medium.com/max/1024/0*SUMnREkdv6KlVAzf&quot; alt=&quot;&quot;&gt;&lt;br&gt;
&lt;em&gt;Photo by Barbara Zandoval on Unsplash&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Writing the script in Bash is a fine choice, the utility does work as expected. Gemini gave me a script that looks maintainable, and like many sane, lazy coders (please disregard the adjective combination), I started piling more requirements to the original problem.&lt;/p&gt;
&lt;p&gt;This experience of treating an LLM chatbot as my Santa does make me wonder — with the overly primitive constructs and toolchain, am I expecting bash to do too much?&lt;/p&gt;
&lt;p&gt;Welcome to the show, where I get Gemini to turn my laziness into cryptic bash code.&lt;/p&gt;
&lt;p&gt;First request was simple, we all like easy things. Though not a needy person, I do want to make myself available to my partner after certain hours. Dear chatbot, do not retry after dinner time.&lt;/p&gt;
&lt;p&gt;The code was generated; Gemini obliged.&lt;/p&gt;
&lt;p&gt;But there were times I needed it for a one-off fling, and the script wouldn’t co-operate. My partner continued to enjoy his victory over my oversight — until another revision to let it simulate a do-while loop, to run at least once and do the I-want-my-me-time guard check after the first iteration.&lt;/p&gt;
&lt;p&gt;Besides interacting with it through the command line interface, the execution of the utility also halts temporarily when it is awaiting input through my cellphone. Think of it as a wellness check procedure; imagine I am preparing to shop for an expensive toy through a website, and it automatically asks for my partner’s approval.&lt;/p&gt;
&lt;p&gt;I feel like someone just said no from a distance, as I am typing this.&lt;/p&gt;
&lt;p&gt;Not just in my head, I hope.&lt;/p&gt;
&lt;p&gt;Before I knew it, the bash script was already spawning subshells, keeping track of the process ID and trapping SIGTERM in case the user wants to jump into his partner’s arms. Getting it to quit when I fail to respond to my cellphone in time turns the script into gibberish that I can no longer comprehend.&lt;/p&gt;
&lt;p&gt;Oh, the generated code was also broken, and I sincerely do not blame Gemini.&lt;/p&gt;
&lt;p&gt;I would whack the user if I were the chatbot.&lt;/p&gt;
&lt;p&gt;Pay for Pro subscription!&lt;/p&gt;
&lt;h4 id=&quot;building-a-non-blocking-process-manager-in-python&quot;&gt;Building a Non-Blocking Process Manager in Python&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn-images-1.medium.com/max/1024/0*VEYwfYPiOfW1ReP9&quot; alt=&quot;&quot;&gt;&lt;br&gt;
&lt;em&gt;Photo by Raja Patel on Unsplash&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;My incompetency with Bash eventually led to a script that I cannot personally maintain. Paying for Pro subscription over this is dumb, and that made me rethink my choices in life. What if I had done this in Python from the very beginning, where it has all the bells and whistles for the job.&lt;/p&gt;
&lt;p&gt;Looking at the monstrosity, I got fed up with getting Gemini to fix it further. Just to be very frank, I couldn’t even pin-point where things were done incorrectly. That was when I reminded myself of the video encoding project I did earlier.&lt;/p&gt;
&lt;p&gt;Essentially, the problem can be written in a coroutine function similar to&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;async def connect(exit_event: asyncio.Event, password: str, timeout : int = KILL_TIMEOUT):
    logger = logging.getLogger('Connect')

while not exit_event.is_set():
        logger.info(&quot;Running utility&quot;)

        await interact(
            await asyncio.create_subprocess_exec(
                'some-utility',
                stdout=asyncio.subprocess.PIPE,
                stderr=asyncio.subprocess.STDOUT,
                stdin=asyncio.subprocess.PIPE
            ),
            password,
            timeout,
            exit_event
        )

        logger.info(&quot;Process exited, waiting before retrying&quot;)
        await asyncio.sleep(RETRY_TIMEOUT)

        if is_past_off_hour():
            logger.info(&quot;Not repeating connection, exiting loop&quot;)
            break

    logger.info(&quot;Exiting connection loop&quot;)
    exit_event.set()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Being increasingly more capable, LLM chatbots took over 70–80% of my programming work. As a result, I forgot that I had autocompletion configured in my editor. After drafting the core coroutine above, I started typing the function signature for all the interaction, and suddenly I was greeted with a somewhat usable draft.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;async def interact(process: asyncio.subprocess.Process, password: str, timeout : int, exit_event: asyncio.Event):
    logger = logging.getLogger('Command output')

    is_success = asyncio.Event()

    tasks = (
        asyncio.create_task(interact_timeout(process, timeout, is_success, exit_event)),
        asyncio.create_task(interact_kill(process, exit_event))
    )

    process.stdin.write(f&quot;{password}\n\n&quot;.encode('utf-8'))

    await process.stdin.drain()

    while process.returncode is None:
        line_bytes = await process.stdout.readline()

        if not line_bytes:
            break

        line = line_bytes.decode('utf-8').strip()
        logger.info(&quot;%s&quot;, line)

        if is_success.is_set() and re.search(r&quot;PARTNER APPROVED&quot;, line):
            is_success.set()

    for task in tasks:
        if task.done() is False:
            task.cancel()

    await process.wait()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Unlike the main coroutine that manages the overall retry-until-me-time setup, the interact coroutine manages each session with the process created by executing the utility. As mentioned, it expects some interaction via the command line interface through STDIN, hence the &lt;code&gt;process.stdin.write&lt;/code&gt; call to input a sequence of characters expected by the utility.&lt;/p&gt;
&lt;p&gt;Capturing the success state is also crucial, and we do that by checking the execution output for a certain string (The string PARTNER APPROVED in the mock example). After the corresponding event is set, we cancel the interaction timeout, as shown below&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;async def interact_timeout(process: asyncio.subprocess.Process, timeout: int, has_dns: asyncio.Event, has_route: asyncio.Event, exit_event: asyncio.Event):
    logger = logging.getLogger('Timeout killer')

    await asyncio.sleep(timeout)

    if not is_success.is_set():
        logger.error(&quot;Timeout waiting for connection, killing process and sending event to exit&quot;)
        process.terminate()
        exit_event.set()
      return
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Lastly, we need to be able to terminate the process whenever desired — such as when we send a SIGTERM to the script.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;async def interact_kill(process: asyncio.subprocess.Process, exit_event: asyncio.Event):
    logger = logging.getLogger('Exit killer')

    await exit_event.wait()

    logger.info(&quot;Exit event set, killing process&quot;)
    process.terminate()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Trapping the SIGTERM event can be done through the snippet shown below, adapted from the snippet &lt;a rel=&quot;nofollow&quot; href=&quot;https://kitfucoda.medium.com/asyncio-task-management-a-hands-on-scheduler-project-bd7b7fe58c7e&quot;&gt;we discussed previously&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://kitfucoda.medium.com/asyncio-task-management-a-hands-on-scheduler-project-bd7b7fe58c7e&quot;&gt;AsyncIO Task Management: A Hands-On Scheduler Project&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;@dataclass
class ShutdownHandler:
    exit_event: asyncio.Event

    def __call__ (
        self, signum: int | None = None, frame: FrameType | None = None
    ) -&amp;gt; None:
        logger = logging.getLogger('ShutdownHandler')

        logger.info(&quot;Sending exit event to all tasks in pool&quot;)

        self.exit_event.set()

        for task in asyncio.all_tasks():
            if task is not asyncio.current_task():
                task.cancel()

exit_event = asyncio.Event()
shutdown_handler = ShutdownHandler(exit_event)

for sig in (signal.SIGINT, signal.SIGTERM):
    asyncio.get_event_loop().add_signal_handler(sig, shutdown_handler)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Seeing the LLM autocompletion feature in action is both a magical and creepy experience. Most of the time it made the work a breeze, especially when it correctly predicted what I had in mind. On the other hand it really felt like someone is watching over my shoulder constantly.&lt;/p&gt;
&lt;p&gt;Developing the script took about an hour, and that included testing to ensure things work as expected. AsyncIO proved to be the right call, as the non-blocking nature ensured the process would terminate if I failed to respond on my cellphone in time. The orchestration would be significantly more complex should we do this synchronously.&lt;/p&gt;
&lt;p&gt;Not like asynchronous code is any easier to read and comprehend anyway.&lt;/p&gt;
&lt;p&gt;Pick your poison.&lt;/p&gt;
&lt;h4 id=&quot;the-verdict-architecture-nix-and-portability&quot;&gt;The Verdict: Architecture, Nix, and Portability&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn-images-1.medium.com/max/1024/0*U3rB6utBgDnlaZ4q&quot; alt=&quot;&quot;&gt;&lt;br&gt;
&lt;em&gt;Photo by Niek Doup on Unsplash&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;I didn’t put in too much effort once I got it running. All I did was taking ideas from my past projects and assemble them together for the use case. No test suite was written, and yet, it worked fine. I guess it is time to answer the core question: was rewriting this in Python the right call?&lt;/p&gt;
&lt;p&gt;It was definitely written to fit my own unique use-case, so there’s no concern about distributing or sharing it with others. Considering it is one of those execute-and-forget (with mostly minimal intervention) programs, having it written in a language I am more comfortable with is crucial. I am sure given enough time and effort, I could get Gemini to generate a usable bash equivalent, but I would rather spend that time on things that matter.&lt;/p&gt;
&lt;p&gt;Like hugging my partner.&lt;/p&gt;
&lt;p&gt;One peculiar discovery during the rewrite was realizing macOS Tahoe is still shipping with Python 3.9. Despite trying to not rely on modern tooling for this humble project (e.g. skipping uv for project management and ruff for linting), the new language constructs brought by 3.10 were too good to miss. Fortunately I already manage my environment with Nix’s home-manager, so setting up 3.10 by default was a no-brainer.&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://blog.stackademic.com/beyond-rbenv-and-pyenv-achieving-reproducible-dev-setups-with-nix-04b610175a59&quot;&gt;Beyond rbenv and pyenv: Achieving Reproducible Dev Setups with Nix&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The implementation of timeout to terminate if I fail to respond to my phone in time is also done deliberately. Such interaction is usually time-sensitive, so if I miss it at first and return after a while it would have been expired anyway. Terminating the process on an assumption that I am absent is therefore a sensible choice.&lt;/p&gt;
&lt;p&gt;As a conclusion to this side-project, yes, rewriting the small humble script in Python is the right call, purely because it makes it maintainable for myself. If you are reading this, the takeaway of this article is hopefully to show how much my partner tolerates my erratic behaviour.&lt;/p&gt;
&lt;p&gt;I meant how AsyncIO can be helpful in such scenario.&lt;/p&gt;
&lt;p&gt;While I didn’t publish the script in full, the snippets shown aren’t that much different from what we discussed in the past. Do let me know if you need help on managing a long-running process through AsyncIO.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://kitfucoda.medium.com/understanding-awaitables-coroutines-tasks-and-futures-in-python-a9c12890d20d&quot;&gt;Understanding Awaitables: Coroutines, Tasks, and Futures in Python&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://kitfucoda.medium.com/asyncio-task-management-a-hands-on-scheduler-project-bd7b7fe58c7e&quot;&gt;AsyncIO Task Management: A Hands-On Scheduler Project&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://kitfucoda.medium.com/concurrency-vs-parallelism-achieving-scalability-with-processpoolexecutor-c366bfcc6207&quot;&gt;Concurrency vs. Parallelism: Achieving Scalability with ProcessPoolExecutor&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Thanks again for reading this far, and I hope to write again soon and get back to Pygame programming&lt;/p&gt;
&lt;p&gt;Finally, I’d like to acknowledge the editorial support provided by Gemini. While the code, architecture, and real-world trial and error are entirely my own, Gemini served as an essential second set of eyes — auditing sentence-to-sentence flow, refining tense consistency, and helping me balance the narrative arc between technical mechanics and personal storytelling. It was a true collaborative effort to ensure this chaotic development journey translates clearly on the page.&lt;/p&gt;
&lt;hr&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/23083/orchestrating-long-running-interactive-subprocesses-with-python-asyncio</guid>
<pubDate>Mon, 20 Jul 2026 15:55:26 +0000</pubDate>
</item>
<item>
<title>Distributed Locking in Python</title>
<link>https://coderlegion.com/12077/distributed-locking-in-python</link>
<description>&lt;p&gt;When making use of Python's &lt;code&gt;asyncio&lt;/code&gt; library, synchronizing code within a single process is a solved problem. A simple &lt;code&gt;asyncio.Lock()&lt;/code&gt; ensures that coroutines play nice together. But as soon as your application scales to multiple containers, servers, or microservices, that local lock becomes invisible to the rest of your fleet.&lt;/p&gt;
&lt;p&gt;You suddenly face the challenge of &lt;strong&gt;Distributed Mutual Exclusion&lt;/strong&gt;: ensuring that a critical section of code is only executed by one worker at a time, regardless of which machine it is running on.&lt;/p&gt;
&lt;p&gt;In this post, we’ll explore a robust distributed lock implementation using &lt;a rel=&quot;nofollow&quot; href=&quot;https://thingsdb.io&quot;&gt;ThingsDB&lt;/a&gt;. This solution provides the familiar syntax of an asyncio context manager but leverages a globally synchronized backend to manage state across your entire infrastructure.&lt;/p&gt;
&lt;h3 id=&quot;why-distributed-locking-is-a-hard-problem&quot;&gt;Why Distributed Locking is a &quot;Hard&quot; Problem&lt;/h3&gt;
&lt;p&gt;On the surface, it sounds easy: just set a flag in a database. But in a distributed environment, the stakes are higher. If a worker acquires a lock and then crashes or loses network connectivity, that lock could remain &quot;held&quot; in the database forever, creating a permanent deadlock.&lt;/p&gt;
&lt;p&gt;ThingsDB solves this by introducing a &lt;strong&gt;server-side&lt;/strong&gt; timeout. If the client holding the lock fails to release it or disappears, ThingsDB automatically releases the lock for the next person in line.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Unlike traditional distributed locks that require &quot;polling&quot; the database, ThingsDB locks are event-driven. They leverage &lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.thingsdb.io/v1/data-types/room/emit/&quot;&gt;ThingsDB’s emit event system&lt;/a&gt;, meaning there is zero wasted overhead. The moment a lock is released, an event triggers the next waiter in line for immediate execution. This ensures maximum throughput while strictly guaranteeing that only one worker is active at a time.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;part-1-the-infrastructure-setup&quot;&gt;Part 1: The Infrastructure Setup&lt;/h3&gt;
&lt;p&gt;Before your application can start locking resources, ThingsDB needs to prepare the underlying collection logic. This is an &lt;strong&gt;idempotent&lt;/strong&gt; operation—it only does work the first time it is called.&lt;/p&gt;
&lt;p&gt;In a production environment, you should perform this setup during your application's bootstrap phase.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from thingsdb.misc import lock

async def initialize_infrastructure(client):
    # Connect and authenticate
    await client.connect('localhost')
    await client.authenticate('admin', 'pass')
    #
    # This prepares the lock collection in ThingsDB.
    # It’s safe to call every time the service starts.
    await lock.setup(client)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;part-2-the-initialize-once-pattern&quot;&gt;Part 2: The &quot;Initialize Once&quot; Pattern&lt;/h3&gt;
&lt;p&gt;In a real-world project, you want to avoid creating new lock objects inside your high-frequency loops. Instead, initialize your lock configuration once and reuse that object throughout the lifecycle of your application.&lt;/p&gt;
&lt;p&gt;By using &lt;code&gt;functools.partial&lt;/code&gt;, we can create a &quot;Lock Factory&quot; that is pre-configured with the correct client, resource name, and timeout settings.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from functools import partial
from thingsdb.misc import lock

# This will hold our pre-configured lock factory
invoice_lock = None

async def setup_application_locks(client):
    global invoice_lock
    #
    # Perform the one-time backend setup
    await lock.setup(client)
    #
    # Initialize the lock object once.
    # If a worker crashes, the lock is freed after 30 seconds.
    invoice_lock = partial(lock.lock,
                           client=client,
                           name='process-invoices',
                           timeout=30)
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;&lt;p&gt;Note: You can create as many different locks as your application needs. Just ensure that each unique resource or critical section is given its own &lt;strong&gt;unique name&lt;/strong&gt; to avoid unintended interference between different parts of your system.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;part-3-using-the-lock-in-your-business-logic&quot;&gt;Part 3: Using the Lock in Your Business Logic&lt;/h3&gt;
&lt;p&gt;Because the implementation uses the &lt;code&gt;async with&lt;/code&gt;syntax, it integrates seamlessly into any &lt;code&gt;asyncio&lt;/code&gt; application. It feels exactly like a local lock, but it’s protecting your data across the globe.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;async def process_invoices():
    # Use the pre-configured lock we created during startup
    async with invoice_lock():
        print('Distributed lock acquired! '
              'No other service can enter this block.')
        #
        # Simulate critical work like hitting a payment API
        await asyncio.sleep(5.0)
        #
        print('Work finished. Lock is automatically released.')
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;monitoring-and-safety&quot;&gt;Monitoring and Safety&lt;/h3&gt;
&lt;p&gt;Sometimes you need to know if a resource is busy without actually waiting in line. You can use the &lt;code&gt;locked()&lt;/code&gt; function for a non-blocking check:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;is_busy = await lock.locked(client, 'process-invoices')
if is_busy:
    print(&quot;Another worker is currently processing invoices.&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;key-takeaways&quot;&gt;Key Takeaways&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Queue Management:&lt;/strong&gt; The &lt;code&gt;queue_size&lt;/code&gt; and &lt;code&gt;timeout&lt;/code&gt; parameters allow you to control exactly how long a request should wait before giving up, preventing &quot;zombie&quot; requests from clogging your system.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Resilience:&lt;/strong&gt; The server-side timeout ensures that your system is self-healing even in the face of hardware failures or process crashes.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Developer Experience:&lt;/strong&gt; By mimicking the &lt;code&gt;asyncio.Lock()&lt;/code&gt; interface, the learning curve for your team is practically zero.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;Distributing your Python application shouldn't mean sacrificing the safety of mutual exclusion. By using ThingsDB to back your locks, you get a high-performance, self-healing synchronization layer that scales with your infrastructure.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/12077/distributed-locking-in-python</guid>
<pubDate>Mon, 23 Feb 2026 07:37:04 +0000</pubDate>
</item>
<item>
<title>The Concurrency Choreographer: Making BLE’s Async Data Sync with OBS</title>
<link>https://coderlegion.com/7761/the-concurrency-choreographer-making-bles-async-data-sync-with-obs</link>
<description>&lt;p&gt;“What can we do with asynchronous programming?”, This was one of the questions I received in my talk on asynchronous programming that I delivered in &lt;a rel=&quot;nofollow&quot; href=&quot;https://kitfucoda.medium.com/from-blog-post-to-pycon-my-one-week-crash-course-in-public-speaking-ab4987b75702&quot;&gt;Pycon MY&lt;/a&gt;. While I briefly mentioned building a chatbot that day. I believe the project we are discussing today should resonate more. During my spare time (however rare it is these days), I stream my gameplay session to document my journey. One of the widgets in my streaming session is a heart rate monitor, and it has been broken for some time.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn-images-1.medium.com/max/1024/1*XhPp68JeBibP6di42WaXHQ.jpeg&quot; alt=&quot;&quot;&gt;&lt;br&gt;
&lt;em&gt;Cute cartoon on the topic generated by Gemini&lt;/em&gt;&lt;/p&gt;
&lt;h4 id=&quot;seeking-the-open-standard-from-fitbit-s-lock-in-to-garmin-s-ble&quot;&gt;Seeking the Open Standard: From Fitbit’s Lock-in to Garmin’s BLE&lt;/h4&gt;
&lt;p&gt;I was doing some spring-cleaning for my OBS setup, and one component caught my eye — the heart rate monitor. For a time, it was a novel way to convey a streamer’s psychological state in a numerical form. Coincidentally, I was already wearing a fitness tracker: Fitbit Versa 3 and I decided to give it a go. Fortunately, there was a way to capture heart rate through the somewhat limited proprietary API, and have it streamed to my computer. Problem is, the fitness (not so) smartwatch died.&lt;/p&gt;
&lt;p&gt;Funnily enough, a family member bought an almost identical Fitbit Sense a few months earlier and her watch is still functioning as of now. In a way the watch died rather prematurely. On the other hand, &lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.to/jeffrey04/beyond-the-code-a-lunar-new-year-reflection-on-career-recovery-and-new-beginnings-l77&quot;&gt;I was out of a job&lt;/a&gt;, and yet that didn’t translate to more gaming time as I was juggling with multiple things at the time. Consequently, the replacement only came much later, not long after I started my current job. Instead of getting another Fitbit, I moved on to a Garmin Instinct II, a tracker my peers highly recommended.&lt;/p&gt;
&lt;p&gt;Unlike my past Fitbit devices, the Garmin watch does broadcast my heart rate via a standard Bluetooth interface. This makes replacing the workflow a much easier job. For that to work, I just needed to figure out how to get the data, and then supply that data to OBS via the WebSocket interface. The library in use is bleak to communicate with the watch via Bluetooth LE, and obsws-python to publish heart rate to OBS.&lt;/p&gt;
&lt;p&gt;Previously, our chatbot project only dealt with asynchronous libraries. Yet, this time the library that communicates with OBS is synchronous. Therefore, we are taking this opportunity to discuss how to coordinate them to work with each other. Specifically, we are solving the problem where the producer (the heart rate monitor data) is asynchronous, and the consumer (the sending of heart rate to OBS) is synchronous.&lt;/p&gt;
&lt;h4 id=&quot;building-the-async-producer-connecting-via-bluetooth-low-energy&quot;&gt;Building the Async Producer: Connecting via Bluetooth Low Energy&lt;/h4&gt;
&lt;p&gt;My journey on tracking my fitness data started with reading about running. One of the books I read was titled &lt;em&gt;Body, Mind and Sport&lt;/em&gt; by John Douillard. Some of the practices advocated may seem controversial, though the idea of being mindful about my physical state stuck in my mind. As a result, I started running with deep breaths, and soon noticed running could be made bearable or even enjoyable with some effort. At the same time, I went looking for a chest belt to pair with my fitness tracker installed on my phone.&lt;/p&gt;
&lt;p&gt;That was before wearable tech went mainstream, so my choices were rather limited. Chest straps to measure heart rate were common among runners, but they weren’t built to work with Bluetooth. I had to search high and low to finally find one that would work with my smartphone application. It was slightly more expensive than an ordinary tracker, but it was worth it. I ran with it for a few years, and benefited from having my current heart rate displayed on my tracker in real time.&lt;/p&gt;
&lt;p&gt;No more estimating by poking my fingers to my neck and doing multiplication while I was busy catching my breath.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn-images-1.medium.com/max/1024/0*hKnfzx0ERmDh3wY1&quot; alt=&quot;&quot;&gt;&lt;br&gt;
&lt;em&gt;Photo by Luiz Rogério Nunes on Unsplash&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;When wearable technology started to take off, Sony later released their smart band. The companion app also provided some limited insight on how my body reacts to activities in my day-to-day life. Conveniently, it also broadcasts my heart rate through Bluetooth and switching to it in my sports tracker took almost no effort. No more having to prepare my chest strap by wetting it before a run — that was a definite win.&lt;/p&gt;
&lt;p&gt;Enough storytelling for now, let’s shift our focus back to the project. Like my old Sony smart band I bought second-hand, my Garmin Instinct 2 also broadcasts my heart rate on Bluetooth on demand. A quick search yielded &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/kieranabrennan/blehrm&quot;&gt;blehrm&lt;/a&gt;, and it provides an abstraction to &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/hbldh/bleak&quot;&gt;bleak&lt;/a&gt;. Unfortunately, it wasn’t the right library for the purpose, and I soon moved on to bleak directly after some failed attempts.&lt;/p&gt;
&lt;p&gt;Firstly, we start by finding our device. Call the BleakScanner.discover method with a reasonable timeout, as shown below:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from bleak import BleakScanner

ble_devices = await BleakScanner.discover(timeout=10.0)
print(f&quot;--- Discovered {len(ble_devices)} BLE Devices ---&quot;)
for i, device in enumerate(ble_devices):
    print(
        f&quot;#{i}: Name: {device.name}, Address: {device.address}, Details: {device}&quot;
    )
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Running that will return (device addresses are replaced with the placeholder &lt;/p&gt;&lt;address&gt;):
&lt;code&gt;--- Discovered 8 BLE Devices ---
#0: Name: Instinct 2, Address: &amp;lt;address&amp;gt;, Details: &amp;lt;address&amp;gt;: Instinct 2
#1: Name: None, Address: &amp;lt;address&amp;gt;, Details: &amp;lt;address&amp;gt;: None
#2: Name: None, Address: &amp;lt;address&amp;gt;, Details: &amp;lt;address&amp;gt;: None
#3: Name: U-WTD838C, Address: &amp;lt;address&amp;gt;, Details: &amp;lt;address&amp;gt;: U-WTD838C
#4: Name: None, Address: &amp;lt;address&amp;gt;, Details: &amp;lt;address&amp;gt;: None
#5: Name: None, Address: &amp;lt;address&amp;gt;, Details: &amp;lt;address&amp;gt;: None
#6: Name: None, Address: &amp;lt;address&amp;gt;, Details: &amp;lt;address&amp;gt;: None
#7: Name: None, Address: &amp;lt;address&amp;gt;, Details: &amp;lt;address&amp;gt;: None
&lt;/code&gt;
&lt;p&gt;Jot down the address, as we will need it for the next step: establishing a connection to the device.&lt;/p&gt;
&lt;code&gt;from bleak import BleakClient

HR_MEASUREMENT_CHARACTERISTIC_UUID = &quot;00002a37-0000-1000-8000-00805f9b34fb&quot;
async with BleakClient(
    &amp;lt;address&amp;gt;,
    timeout=15.0
) as client:
    await client.start_notify(
        HR_MEASUREMENT_CHARACTERISTIC_UUID,
        heart_rate_handler,
    )
&lt;/code&gt;
&lt;p&gt;In the snippet, we are sending the address we collected earlier to BleakClient with a reasonable timeout. Then we subscribe to the heart rate measurement event, and delegate the handling to a function.&lt;/p&gt;
&lt;p&gt;With the help of Gemini, we saved time digging through the documentation, as it returned a ready-made snippet for the BLE connection and data parsing, which is shown below.&lt;/p&gt;
&lt;code&gt;def heart_rate_handler(sender, data):
    &quot;&quot;&quot;Callback to handle heart rate measurement data.&quot;&quot;&quot;
    # The heart rate data structure is defined in the BLE spec:
    # Byte 0: Flags (contains format of HR value and if other fields are present)
    # Byte 1: Heart Rate Value (8-bit or 16-bit)

    # Check the first bit of the flags to see if HR is 16-bit (0x01) or 8-bit (0x00)
    flags = data[0]
    is_16_bit = flags &amp;amp; 0x01
    if is_16_bit:
        # 16-bit HR value is at index 1 and 2
        hr_value = unpack(&quot;&amp;lt;H&quot;, data[1:3])[0]
    else:
        # 8-bit HR value is at index 1
        hr_value = data[1]
    logger.info(f&quot;[{flags:04b}] Heart Rate: {hr_value} BPM&quot;)
&lt;/code&gt;
&lt;p&gt;There we have it, we have half the program done, and it is able to fetch heart rate data periodically from the device.&lt;/p&gt;
Bridging the Gap: The Producer-Consumer Pattern
&lt;p&gt;&lt;img src=&quot;https://cdn-images-1.medium.com/max/1024/0*iPoTtRyufsJY7PGx&quot; alt=&quot;&quot;&gt;&lt;br&gt;
&lt;em&gt;Photo by Lenzil Gonsalves on Unsplash&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Continuing the story after I bought my second-hand Sony smart band. Electronics products die inevitably, as Sony wasn’t making a viable successor for the product I had to look elsewhere. That was when I first looked at Fitbit, particularly a Fitbit Charge 3. It was more than doubled the price of my smart band, though it came with more features. One feature I particularly liked was the sleep tracker, as I have really bad sleep pattern. It was working fine, though with a major drawback — I lost the ability to read my heart rate through the sports tracker app.&lt;/p&gt;
&lt;p&gt;Since the device came with a screen itself, it did mean there was no need to whip out my phone constantly to check my heart rate. Not being able to log my run with heart rate data attached created a minor annoyance. On the other hand, I moved on to other sports already, namely boxing and weightlifting. The shift in activity did change the need as I didn’t need the data as often. For instance, the short-burst nature of weightlifting did not require constant monitoring of my heart rate; the main focus was just on whether I could lift the dumbbell or otherwise.&lt;/p&gt;
&lt;p&gt;Again, the tracker died after serving for several years. I moved on to a Versa 3 on a good deal, despite the mild annoyance. Other brands were considered, but I got confused with the numerous lines of products and decided to stay with Fitbit for another time. Being a smart watch, Fitbit did release an SDK for their Versa watches, but they discontinued application development support when they released the successor, Versa 4. Regrettably, I didn’t put in the time and effort to develop anything as I was adequately happy with it.&lt;/p&gt;
&lt;p&gt;As mentioned earlier, I found a way to broadcast my heart rate with an app offered in the Fitbit app store, and that led to the setup of a heart rate widget on my OBS setup. Coincidentally, I acquired Tetris Effect on my workstation, so I was curious to see if my heart rate changed throughout the game play. Surprisingly, it really did, and the recording clearly showed the heart rate fluctuations in action.&lt;/p&gt;
&lt;p&gt;&lt;iframe src=&quot;https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fwww.youtube.com%2Fembed%2FXtSY_DvsM-E%3Ffeature%3Doembed&amp;amp;display_name=YouTube&amp;amp;url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DXtSY_DvsM-E&amp;amp;image=https%3A%2F%2Fi.ytimg.com%2Fvi%2FXtSY_DvsM-E%2Fhqdefault.jpg&amp;amp;type=text%2Fhtml&amp;amp;schema=youtube&quot; width=&quot;854&quot; height=&quot;480&quot; frameborder=&quot;0&quot; scrolling=&quot;no&quot;&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://medium.com/media/018de8b0f30246afd93165f500a77b62/href&quot;&gt;&lt;/a&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://medium.com/media/018de8b0f30246afd93165f500a77b62/href&amp;lt;/a&amp;gt;&amp;lt;/iframe&amp;gt;&quot;&gt;https://medium.com/media/018de8b0f30246afd93165f500a77b62/href&amp;lt;/a&amp;gt;&amp;lt;/iframe&amp;gt;&lt;/a&gt;&lt;/iframe&gt;&lt;/p&gt;
&lt;p&gt;Granted, as the tracker was worn on the wrist, the response was somewhat delayed. Despite that, the stream showed as the tetrimino dropped faster, my heart rate also increased correspondingly. More often than not, the height of dropped blocks waiting to be eliminated also played a role in influencing the heart rate.&lt;/p&gt;
&lt;p&gt;Now that we have the stream of heart rate data, how do we recreate the widget in OBS through our script?&lt;/p&gt;
&lt;p&gt;Open Broadcasting Studio, or OBS in short, offers a way to interact with the sources on screen through the websocket interface. In our case, we are interested in sending our heart rate data to a text source. By using the obsws-python library, we can send the data as soon as we capture it from the device. We can establish this connection to OBS using the following simple code:&lt;/p&gt;
&lt;code&gt;from obsws_python import ReqClient

obs = ReqClient(
 host=&amp;lt;host&amp;gt;,
 port=&amp;lt;port&amp;gt;,
 password=&amp;lt;password&amp;gt;,
)
&lt;/code&gt;
&lt;p&gt;All of these settings can be found in the OBS websocket setup page, as shown in the screenshot below:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn-images-1.medium.com/max/1024/1*smsVfNpsIrpHQrGgkfu8hg.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;Sending heart rate to the source is simple, through the obs.set_input_settings method&lt;/p&gt;
&lt;code&gt;obs.set_input_settings(
 &amp;lt;identifier of the target source&amp;gt;,
 {“text”: f”{item:3d}”},
 True,
)
&lt;/code&gt;
&lt;p&gt;By assembling all these together we would have a complete program for the job. The only problem with putting everything together is that we are mixing asynchronous code calling bleak with a strictly synchronous and likely non thread-safe code by obsws-python. Fortunately, the data only flows one-way; otherwise it complicates the whole setup significantly more. Ideally the library that dealt with OBS websocket could be written in a more asynchronous manner but obsws-python was the only reliable library that I could find in a weekend.&lt;/p&gt;
&lt;p&gt;The project started as a weekend project, and I stopped right after achieving a running proof-of-concept. Only later, when I finally had the time, did I start refactoring this into a more reliable program that properly handles errors and graceful shutdown. Considering we discussed rather extensively in the past on those topics, let’s skip to how we turn this into a producer-consumer design.&lt;/p&gt;
&lt;code&gt;with ThreadPoolExecutor(max_workers=5) as executor:
   executor.submit(producer)
   executor.submit(consumer)
&lt;/code&gt;
&lt;p&gt;We want both the consumer and producer to remain isolated from each other, so we separate them into individual threads. Usually it is better to split them into different processes, but since we are only running one event loop for the producer part it is okay to stick with threads. In order to synchronize both of them, we need to pass a queue and an exit event to them as well. This can be achieved with functools.partial, or by writing a class with the dunder __call__ method, a topic we &lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.to/jeffrey04/the-versatility-of-call-a-python-developers-secret-weapon-4akd&quot;&gt;discussed previously&lt;/a&gt;. I will leave the implementation detail out to focus on the core concept itself.&lt;/p&gt;
&lt;p&gt;Now, back to our producer, where we collect the heart rate, instead of printing the heart rate, we dump it to our queue:&lt;/p&gt;
&lt;code&gt;logger.info(f”[{flags:04b}] Heart Rate: {hr_value} BPM”)
asyncio.create_task(asyncio.to_thread(queue.put, hr_value))
&lt;/code&gt;
&lt;p&gt;In case you missed our series of &lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.to/jeffrey04/concurrency-vs-parallelism-achieving-scalability-with-processpoolexecutor-1n7n&quot;&gt;articles&lt;/a&gt; on &lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.to/jeffrey04/concurrency-vs-parallelism-achieving-scalability-with-processpoolexecutor-1n7n&quot;&gt;AsyncIO&lt;/a&gt; (go read them, they are awesome!), queue.put is a blocking operation, so we are delegating it to asyncio.to_thread. For our purpose, we don’t exactly care about the outcome, but we want to ensure it is scheduled safely. Therefore, we pass the returned coroutine object to asyncio.create_task.&lt;/p&gt;
&lt;p&gt;On the other hand, as the code dealing with OBS is synchronous, we need to plan carefully.&lt;/p&gt;
&lt;code&gt;def consumer(queue: Queue, exit_event: Event) -&amp;gt; None:
    obs = ReqClient(
        host=&amp;lt;host&amp;gt;,
        port=&amp;lt;port&amp;gt;,
        password=&amp;lt;password&amp;gt;,
    )

    logger.info(&quot;Starting consumption loop&quot;)
    while True:
        # task 1: handle the exit event
        if exit_event.is_set():
            logger.info(&quot;Exiting consumption loop&quot;)
            break

        # task 2: publish to obs
        with suppress(Empty):
            if item := queue.get(timeout=5.0):
                obs.set_input_settings(
                    &amp;lt;identifier of the target source&amp;gt;,
                    {&quot;text&quot;: f&quot;{item:3d}&quot;},
                    True,
                )
&lt;/code&gt;
&lt;p&gt;This simplified snippet sets up an infinite loop that does two things, firstly it checks if the exit_event is triggered, and secondly, it consumes the heart rate data from the queue and sends it to OBS. We would need to set a timeout and suppress the potential Empty exception when getting a value from the queue. The reason for this is otherwise the code will block until it receives a data, and we would never be able to capture exit_event that was emitted elsewhere.&lt;/p&gt;
Asyncio’s Role in Real-World, Non-Web I/O
&lt;p&gt;Essentially, this is a fairly common Consumer-Producer application between two different components. While the consumer is depending on the producer to publish to OBS, the producer should not have to wait for it to complete. By separating them into different threads and using a queue for synchronization, we allowed both components to work independently. Unlike the telegram chatbot we built previously, this shows how we can adapt synchronous code into an asynchronous environment.&lt;/p&gt;
&lt;p&gt;I appreciate the fact that Garmin allows the heart rate data to be made available through an open interface. If I were to get a new Versa watch, this project would not have been possible as the data is locked away from a proprietary API. As mentioned earlier, Fitbit also removed support for application development for the newer models.&lt;/p&gt;
&lt;p&gt;That would mean no more Tetris Effect streams with heart rate readings for me.&lt;/p&gt;
&lt;p&gt;&lt;iframe src=&quot;https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fwww.youtube.com%2Fembed%2FXSC61Y4UzwY%3Ffeature%3Doembed&amp;amp;display_name=YouTube&amp;amp;url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DXSC61Y4UzwY&amp;amp;image=https%3A%2F%2Fi.ytimg.com%2Fvi%2FXSC61Y4UzwY%2Fhqdefault.jpg&amp;amp;type=text%2Fhtml&amp;amp;schema=youtube&quot; width=&quot;854&quot; height=&quot;480&quot; frameborder=&quot;0&quot; scrolling=&quot;no&quot;&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://medium.com/media/6fd1166a20e17dcd450a99014354e282/href&quot;&gt;&lt;/a&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://medium.com/media/6fd1166a20e17dcd450a99014354e282/href&amp;lt;/a&amp;gt;&amp;lt;/iframe&amp;gt;&quot;&gt;https://medium.com/media/6fd1166a20e17dcd450a99014354e282/href&amp;lt;/a&amp;gt;&amp;lt;/iframe&amp;gt;&lt;/a&gt;&lt;/iframe&gt;&lt;/p&gt;
&lt;p&gt;Not like anyone cared anyway.&lt;/p&gt;
&lt;p&gt;So, is this project a good example as a response to the question posed early in the article? Is this a good demonstration of what asynchronous programming is capable of? I would argue yes, as this shows even though not all components are asynchronous, it is still possible to workaround it. When we have multiple components that need to talk to each other, asyncio is definitely a way to go, albeit requiring more thought and work into it.&lt;/p&gt;
&lt;p&gt;That’s all I have for today! The code snippets are purposefully simplified for brevity, but if you are interested in the project, feel free to check it out on &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/Jeffrey04/obs-garmin/&quot;&gt;GitHub&lt;/a&gt;. Thanks for reading, I shall see you in writing again soon.&lt;/p&gt;
&lt;p&gt;For the sake of transparency, this article was a collaborative effort with my editor, Gemini, who assisted me throughout the writing process. Beyond providing editorial suggestions to tighten the narrative and structure, the model was instrumental in accelerating the technical side of the project, specifically by generating and verifying the initial code snippet required to correctly extract and parse the raw heart rate data from the BLE characteristic — a small but critical step that saved me hours of digging through specification sheets. The final code and the story are mine.&lt;/p&gt;

&lt;/address&gt;</description>
<category>Synchronization</category>
<guid isPermaLink="true">https://coderlegion.com/7761/the-concurrency-choreographer-making-bles-async-data-sync-with-obs</guid>
<pubDate>Tue, 25 Nov 2025 03:14:25 +0000</pubDate>
</item>
<item>
<title>Asynchronous Python: A Beginner’s Guide to asyncio</title>
<link>https://coderlegion.com/6407/asynchronous-python-a-beginners-guide-to-asyncio</link>
<description>&lt;p&gt;&lt;strong&gt;Howdy Folks:&lt;/strong&gt; Ever wondered about asynchronous processes in programming? Join us on a journey into the realm of asynchronous Python with a focus on the asyncio module. We’ll demystify the concepts, delve into syntax, and showcase how asyncio empowers concurrent task execution. Get ready to harness the power of asynchronous Python and elevate your programming prowess!&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What is an Asynchronous process anyways?&lt;/strong&gt;&lt;br&gt;
In the world of programming, there are tasks that take varying amounts of time to complete. Some tasks may be quick, while others might require waiting for external resources, such as data from a server or user input. As developers, we often encounter situations where we need to perform multiple tasks concurrently without waiting for each one to finish before starting the next. This is where asynchronous programming comes into play, and the &lt;code&gt;asyncio&lt;/code&gt; module in Python is a powerful tool to achieve this.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Importing the asyncio Module&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Ok folks, the first step in utilizing the &lt;code&gt;asyncio&lt;/code&gt; module is to import it into our Python script. This can be done using the following line of code:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import asyncio
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This line makes all the asynchronous magic provided by the &lt;code&gt;asyncio&lt;/code&gt; module available for use in our script. easy peasy, lemon squeasy, right?&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Understanding Asynchronous Tasks&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;In traditional synchronous programming, tasks are executed one after the other, and the program waits for each task to complete before moving on to the next. In asynchronous programming, we can mark certain functions as asynchronous using the &lt;code&gt;async&lt;/code&gt; keyword, allowing them to run concurrently with other tasks.&lt;/p&gt;
&lt;p&gt;Let’s take a simple example to illustrate this concept, folks:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;async def task1():
    print(&quot;Wake Up started&quot;)
    await asyncio.sleep(1)
    print(&quot;Wake Up completed&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Oh, what is that? Ok, allow me to explain it: In this code, the &lt;code&gt;async def&lt;/code&gt; declaration indicates that the function &lt;code&gt;task1&lt;/code&gt; is asynchronous. The &lt;code&gt;await asyncio.sleep(1)&lt;/code&gt; line simulates a task that takes 1 second to complete. While waiting for this task, other asynchronous functions can execute.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The Main Function and asyncio.gather&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Now, in order to coordinate and execute multiple asynchronous tasks, we use the &lt;code&gt;asyncio.gather&lt;/code&gt; method within a main asynchronous function.&lt;/p&gt;
&lt;p&gt;Let’s take a look at this example, champs:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;async def main():
    await asyncio.gather(task1(), task2(), task3())
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In this code, the &lt;code&gt;main&lt;/code&gt; function is marked as asynchronous using &lt;code&gt;async def&lt;/code&gt;. The &lt;code&gt;asyncio.gather&lt;/code&gt; method is then used to execute the tasks &lt;code&gt;task1&lt;/code&gt;, &lt;code&gt;task2&lt;/code&gt;, and &lt;code&gt;task3&lt;/code&gt; concurrently. The &lt;code&gt;await&lt;/code&gt; keyword is used before &lt;code&gt;asyncio.gather&lt;/code&gt; to ensure that the program waits for the completion of all tasks.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Bringing It All Together&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Now, let’s put everything together into a complete script:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import asyncio

async def task1():
    print(&quot;Wake Up started&quot;)
    await asyncio.sleep(1)
    print(&quot;Wake Up completed&quot;)

async def task2():
    print(&quot;Take a shower started&quot;)
    await asyncio.sleep(2)
    print(&quot;Take a shower completed&quot;)

async def task3():
    print(&quot;Have Breakfast started&quot;)
    await asyncio.sleep(4)
    print(&quot;Have Breakfast completed&quot;)

async def main():
    await asyncio.gather(task1(), task2(), task3())

asyncio.run(main())
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When you run this script, you’ll see the output reflecting the asynchronous execution of tasks. Each task begins, and while waiting for the time-delayed &lt;code&gt;sleep&lt;/code&gt; to complete, other tasks proceed concurrently.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Wrapping Up&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;In this article, we explored the basics of asynchronous programming in Python using the &lt;code&gt;asyncio&lt;/code&gt; module. We learned how to mark functions as asynchronous, use the &lt;code&gt;await&lt;/code&gt; keyword to wait for asynchronous tasks, and coordinate multiple tasks using &lt;code&gt;asyncio.gather&lt;/code&gt;. Asynchronous programming enables more efficient use of resources and improves the responsiveness of applications, making it a valuable skill for developers working on various projects.&lt;/p&gt;
&lt;p&gt;I trust you found this article both educational and enjoyable. I’ve strived to simplify the content for a quick and effective learning experience. Happy coding, and may your Python endeavors always thrive in your daily life!&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Enjoyed the article? Your feedback fuels more content.&lt;/li&gt;
&lt;li&gt;Share your thoughts in a comment.&lt;/li&gt;
&lt;li&gt;No time to read now? Well, Bookmark for later.&lt;/li&gt;
&lt;li&gt;If it helped, click on share &amp;amp; pass it on, dude!&lt;/li&gt;
&lt;/ul&gt;
</description>
<category>Python</category>
<guid isPermaLink="true">https://coderlegion.com/6407/asynchronous-python-a-beginners-guide-to-asyncio</guid>
<pubDate>Tue, 14 Oct 2025 05:51:37 +0000</pubDate>
</item>
<item>
<title>Blocking vs Non-blocking vs Asynchronous I/O</title>
<link>https://coderlegion.com/4375/blocking-vs-non-blocking-vs-asynchronous-i-o</link>
<description>&lt;p&gt;When a program performs I/O → like reading from a file or socket → two key questions arise:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Does the program stop and wait for the data, or continue running? (&lt;strong&gt;Blocking vs Non-blocking IO&lt;/strong&gt;)&lt;/li&gt;
&lt;li&gt;Does the program keep checking for the result, or get notified when it’s done? (&lt;strong&gt;Synchronous vs Asynchronous IO&lt;/strong&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These are orthogonal concepts, meaning they can be mixed in different combinations. Each combination comes with trade-offs in &lt;strong&gt;performance, complexity, and responsiveness&lt;/strong&gt;. In this article, we'll break down these models to understand how I/O really works under the hood.&lt;/p&gt;
&lt;h3 id=&quot;blocking-vs-non-blocking-i-o-intuition&quot;&gt;Blocking vs Non-blocking I/O Intuition&lt;/h3&gt;
&lt;p&gt;&lt;em&gt;&lt;strong&gt;Question to ask&lt;/strong&gt; → After placing the coffee order, do you stand there waiting until it’s ready, or do you walk away and do other things in the meantime?&lt;/em&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Blocking:&lt;/strong&gt; You stand at the coffee counter and wait until your coffee is ready before leaving.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Non-blocking:&lt;/strong&gt; You place your order and then walk away; if the coffee isn’t ready yet, you don’t wait → you might come back later to check.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;synchronous-vs-asynchronous-i-o-intuition&quot;&gt;Synchronous vs Asynchronous I/O Intuition&lt;/h3&gt;
&lt;p&gt;&lt;em&gt;&lt;strong&gt;Question to ask&lt;/strong&gt; → After placing the coffee order, do you keep checking if it’s ready, or do they notify you when it’s done?&lt;/em&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Synchronous:&lt;/strong&gt; You keep walking back to the counter every few minutes to ask, “Is my coffee ready yet?”&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Asynchronous:&lt;/strong&gt; You leave and go about your day → when your coffee is ready, they text you so you know to come back.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt; - It’s important to understand that &lt;strong&gt;asynchronous&lt;/strong&gt; and &lt;strong&gt;non-blocking&lt;/strong&gt; are related but different concepts:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Non-blocking I/O is about &lt;strong&gt;waiting vs not waiting&lt;/strong&gt; → whether the program waits (blocks) for the operation to complete or the call returns immediately if data isn’t ready.&lt;/li&gt;
&lt;li&gt;Asynchronous I/O is about &lt;strong&gt;who drives the control flow&lt;/strong&gt; → whether the program itself keeps checking (synchronous) or the system notifies the program when the operation completes (asynchronous).&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;blocking-i-o-implementation---using-read-&quot;&gt;Blocking I/O Implementation - Using read()&lt;/h3&gt;
&lt;p&gt;The call waits until completion before returning, as shown in the diagram below.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Blocking read
int n = read(fd, buffer, size); // Blocks until data is ready
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2un1x1gj51shm2czc6ss.png&quot; alt=&quot;Blocking read()&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;non-blocking-i-o-synchronous-implementation-1-using-read-&quot;&gt;Non-blocking I/O Synchronous Implementation 1 – Using read()&lt;/h3&gt;
&lt;p&gt;Your program calls &lt;strong&gt;read()&lt;/strong&gt; again and again in a loop. If there’s no data, &lt;strong&gt;read()&lt;/strong&gt; returns -1 with EAGAIN. This wastes CPU cycles because you're periodically checking by issuing read() system calls. But between checks, your program can do other things (&lt;strong&gt;non-blocking&lt;/strong&gt;). Your program drives the control flow → it decides when to check for the data availability (&lt;strong&gt;synchronous&lt;/strong&gt;).&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fcntl(fd, F_SETFL, O_NONBLOCK);  // Set fd to non-blocking

while (1) {
    int n = read(fd, buffer, sizeof(buffer));
    if (n &amp;gt; 0) {
        // Got data
        handle_data(buffer, n);
        break;
    } else if (n == -1 &amp;amp;&amp;amp; (errno == EAGAIN || errno == EWOULDBLOCK)) {
        // No data, do something else
        do_other_work();
    } else {
        // Some other error
        break;
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;non-blocking-i-o-synchronous-implementation-2-using-select-poll-&quot;&gt;Non-blocking I/O Synchronous Implementation 2 – Using select()/poll()&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;select()&lt;/strong&gt; is used to multiplex a set of file descriptors → allowing your program to wait efficiently until &lt;strong&gt;any one&lt;/strong&gt; of them is ready for I/O. Unlike repeatedly calling non-blocking read(), which issues a system call each time and wastes CPU cycles when no data is available, select() makes a single blocking system call that &lt;strong&gt;sleeps&lt;/strong&gt; (means no cpu occupied) until at least one descriptor is ready. After select() returns, checking which descriptors are ready using &lt;strong&gt;FD_ISSET&lt;/strong&gt; is a fast &lt;strong&gt;user-space&lt;/strong&gt; operation that doesn’t incur extra system calls, making the whole process much more efficient.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;while (1) {
    fd_set fds;
    FD_ZERO(&amp;amp;fds);
    FD_SET(fd1, &amp;amp;fds);
    FD_SET(fd2, &amp;amp;fds);
    int max_fd = (fd1 &amp;gt; fd2) ? fd1 : fd2;

    // Block until one of the FDs is ready to read
    if (select(max_fd + 1, &amp;amp;fds, NULL, NULL, NULL) &amp;gt; 0) {
        if (FD_ISSET(fd1, &amp;amp;fds)) {
            // fd1 has data
            read(fd1, buffer1, sizeof(buffer1));
        }
        if (FD_ISSET(fd2, &amp;amp;fds)) {
            // fd2 has data
            read(fd2, buffer2, sizeof(buffer2));
        }
    }
    // You can also perform other logic here
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;summary-so-far&quot;&gt;Summary So Far&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rimjz3jgh7dx8aq4doso.png&quot; alt=&quot;Summary So Far&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;non-blocking-asynchronous-i-o-implementation-using-os-provided-async-apis-e-g-io-uring-windows-iocp-linux-aio-&quot;&gt;Non-blocking Asynchronous I/O Implementation — Using OS-Provided Async APIs (e.g., io_uring, Windows IOCP, Linux AIO)&lt;/h3&gt;
&lt;p&gt;In asynchronous I/O, the program initiates the I/O operation and &lt;strong&gt;does not&lt;/strong&gt; check or wait for the result. Instead, the OS &lt;strong&gt;notifies&lt;/strong&gt; the program (via callbacks, signals, or event queues) when the operation completes, handing control back to the program only when data is ready. This allows maximum concurrency and responsiveness, as your program never blocks or polls.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Initiate async read operation
async_read(fd, buffer, size, callback_function);

// Meanwhile, do other work here

// callback_function is called by OS when data is ready
void callback_function(int result, char* buffer) {
    if (result &amp;gt; 0) {
        handle_data(buffer, result);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/e6lidowy77vabsmkm3jd.png&quot; alt=&quot;Async IO&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;final-summary&quot;&gt;Final Summary&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/osx1z6k8u914d3q6t380.png&quot; alt=&quot;Final Summary&quot;&gt;&lt;/p&gt;
</description>
<category>Disk Mgmt</category>
<guid isPermaLink="true">https://coderlegion.com/4375/blocking-vs-non-blocking-vs-asynchronous-i-o</guid>
<pubDate>Fri, 01 Aug 2025 21:58:27 +0000</pubDate>
</item>
<item>
<title>Nexios (ASGI Python framework)</title>
<link>https://coderlegion.com/3989/nexios-asgi-python-framework</link>
<description>&lt;h2 id=&quot;nexios-a-modern-python-web-framework-built-for-speed&quot;&gt;Nexios – A Modern Python Web Framework Built for Speed&lt;/h2&gt;
&lt;h3 id=&quot;the-story-behind-nexios&quot;&gt;The Story Behind Nexios&lt;/h3&gt;
&lt;p&gt;Nexios was born out of frustration — frustration with bulky frameworks, unnecessary boilerplate, and the over-complication of simple tasks. I wanted a framework that had:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;the simplicity of &lt;strong&gt;Express.js&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;the power of &lt;strong&gt;FastAPI&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;the flexibility of &lt;strong&gt;raw ASGI&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;and none of the magic Django-style meta-classes.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So I built &lt;strong&gt;Nexios&lt;/strong&gt; — a lightweight, async-first Python web framework designed to help developers ship fast, clean, and production-ready APIs with minimal setup.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;-how-nexios-works&quot;&gt;⚙️ How Nexios Works&lt;/h3&gt;
&lt;p&gt;Nexios is built on ASGI and designed with speed and clarity in mind. You define routes using Python functions — no decorators, no hidden base classes, no magic. Just clean, readable code.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from nexios import NexiosApp

app = NexiosApp()

@app.route(&quot;/hello&quot;)
async def hello(request, response):
    return {&quot;message&quot;: &quot;Hello, Nexios!&quot;}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Under the hood, Nexios handles routing, request parsing, async middleware, response formatting, and more — all in a modular and blazing-fast ASGI engine.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;key-features&quot;&gt;Key Features&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;✅ &lt;strong&gt;Async-first&lt;/strong&gt;: Built on ASGI for native async performance.&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Minimal boilerplate&lt;/strong&gt;: Get up and running in 5 lines of code.&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Custom Auth Backends&lt;/strong&gt;: JWT, API keys, token rotation, blacklisting — all pluggable.&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;ORM-Agnostic&lt;/strong&gt;: Works with Tortoise, Prisma, SQLAlchemy, or no ORM at all.&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Zero Magic&lt;/strong&gt;: No hidden behavior or metaclass gymnastics.&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Fast Startup&lt;/strong&gt;: Instant dev server, instant reload.&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Auto Docs&lt;/strong&gt;: Generate OpenAPI docs without extra setup.&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Extensible Core&lt;/strong&gt;: Built to be hacked and extended.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;how-to-get-involved&quot;&gt;How to Get Involved&lt;/h3&gt;
&lt;p&gt;Nexios is open-source and community-friendly. You can:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;⭐ &lt;strong&gt;Star the repo&lt;/strong&gt; on GitHub&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Report bugs&lt;/strong&gt; or request features&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Contribute modules&lt;/strong&gt; or plugins&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Join discussions&lt;/strong&gt; in the community&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;GitHub: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/nexios-labs/Nexios&quot;&gt;https://github.com/nexios-labs/Nexios&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Docs: &lt;a rel=&quot;nofollow&quot; href=&quot;[https://nexios-docs.netlify.app&quot;&gt;https://nexios-docs.netlify.app&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;ready-to-try-it-&quot;&gt;Ready to Try It?&lt;/h3&gt;
&lt;p&gt;Install it now:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;pip install nexios
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Create a new project:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;nexios new  &amp;lt;project_name&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then start building APIs your way.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;Built by developers, for developers. Nexios is what Python web development &lt;em&gt;should&lt;/em&gt; feel like.&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
</description>
<category>Open Source</category>
<guid isPermaLink="true">https://coderlegion.com/3989/nexios-asgi-python-framework</guid>
<pubDate>Mon, 14 Jul 2025 12:14:49 +0000</pubDate>
</item>
<item>
<title>The Pygame Framework I Didn’t Plan: Building Tic-Tac-Toe with Asyncio and Events</title>
<link>https://coderlegion.com/3729/the-pygame-framework-i-didnt-plan-building-tic-tac-toe-with-asyncio-and-events</link>
<description>&lt;p&gt;Over the last weekend, I spent some time mucking around with &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.pygame.org/news&quot;&gt;Pygame&lt;/a&gt;. Despite facing some challenges along the way, I ended up with a working prototype. There was neither design nor specification drafted for the project. Nonetheless, mid-development, a reusable design emerged, and we discussed that in last week’s article. Following that discovery, we will explore the implementation this week and answer the question ‚ “Can we make a framework out of it?”.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn-images-1.medium.com/max/1024/1*EFBQUlLgruEaJ-xBPLUYCQ.png&quot; alt=&quot;&quot;&gt;&lt;br&gt;
&lt;em&gt;Cute illustration from Copilot&lt;/em&gt;&lt;/p&gt;
&lt;h4 id=&quot;the-unexpected-framework-from-pygame-hacks-to-a-reusable-design&quot;&gt;The Unexpected Framework: From Pygame Hacks to a Reusable Design&lt;/h4&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.to/jeffrey04/understanding-awaitables-coroutines-tasks-and-futures-in-python-gk7&quot;&gt;Understanding Awaitables: Coroutines, Tasks, and Futures in Python&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;As a brief recap to the &lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.to/jeffrey04/my-pygame-evolution-embracing-asyncio-and-immutability-for-scalable-design-g55&quot;&gt;last article&lt;/a&gt;, we know how Pygame is designed to be primitive, and lacks inherent structure. Building a Pygame application would therefore require a developer to begin by writing the main application loop. On the other hand, this also presents an opportunity to design a bespoke structure for graphical application development. By refactoring our work from last week, we will witness how a framework emerges, while adhering the original design goal to stay immutable and reliable.&lt;/p&gt;
&lt;p&gt;Adapting the synchronous main application loop to AsyncIO took quite some effort. Despite the complexity involved, it was worth it because it broke the tight coupling of display update and event dispatching loop. Previously, events were only fetched once per display update cycle. If the application was written in a pure event driven manner, delegating work into multiple events would introduce latency.&lt;/p&gt;
&lt;p&gt;Now that both event dispatching and display update run independently, ensuring synchronization of application states between the two components is also crucial. Immutability was therefore introduced such that application data can only be changed through publishing changes to relevant queues.&lt;/p&gt;
&lt;p&gt;With all the work laid out, this opens up an interesting potential, allowing the orchestration code for event dispatching and display updates to be split from the game logic. Application setup work, such as event handler registrations and the initialization of application-specific data, is the primary area that differs from one application to another.&lt;/p&gt;
&lt;p&gt;Sounds like a plan, let’s split it out, and complete the tic-tac-toe game we started.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn-images-1.medium.com/max/1024/0*ZM4TOBEllAcdECvK&quot; alt=&quot;&quot;&gt;&lt;br&gt;
&lt;em&gt;Photo by Daniel McCullough on Unsplash&lt;/em&gt;&lt;/p&gt;
&lt;h4 id=&quot;building-the-core-immutable-state-and-asynchronous-orchestration&quot;&gt;Building the Core: Immutable State and Asynchronous Orchestration&lt;/h4&gt;
&lt;p&gt;It shouldn’t be difficult, right? Given the design goal mentioned earlier, the splitting work was indeed not too difficult. On the other hand, keeping track of the declared dataclasses is another story. Regardless, we will uncover an architectural pattern that forms the framework core through some refactoring work. Some minor revisions will be made to the state management, as well as the main application loop. All this work yields a reusable framework for simple graphical applications.&lt;/p&gt;
&lt;p&gt;Previously we were creating multiple queues for different fields in our Application dataclass. Having multiple queues serving similar purpose to update values is fine, though I personally find it wasteful. Why not just consolidate them?&lt;/p&gt;
&lt;p&gt;And I did exactly that.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn-images-1.medium.com/max/1024/0*pbPfF-uj2A66b_aR&quot; alt=&quot;&quot;&gt;&lt;br&gt;
&lt;em&gt;Photo by USGS on Unsplash&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Firstly, we start by introducing an Enum to represent the field we intend to update&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class ApplicationDataField(Enum):
    STATE = auto()
    ELEMENTS = auto()
    EVENTS = auto()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Secondly, we update the DeltaOperation to always require the field we intend to update&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@dataclass
class DeltaOperation(ABC):
    field: ApplicationDataField
    item: Any
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Lastly, we need to update the application_refresh function, which is the only place where the Application object is updated throughout the framework.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;async def application_refresh(application: Application) -&amp;gt; Application:
    result = application

    with suppress(asyncio.queues.QueueEmpty):
        while delta := application.delta_data.get_nowait():
            field = application_get_field(application, delta.field)

            match delta:
                case DeltaAdd():
                    result = replace(
                        result,
                        **{field: getattr(result, field) + (delta.item,)},
                    )

                case DeltaUpdate():
                    result = replace(
                        result,
                        **{
                            field: tuple(
                                delta.new if item == delta.item else item
                                for item in getattr(result, field)
                            )
                        },
                    )

                case DeltaDelete():
                    result = replace(
                        result,
                        **{
                            field: tuple(
                                item
                                for item in getattr(result, field)
                                if not item == delta.item
                            )
                        },
                    )

    return result
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Actual implementation is a little bit longer, but I truncated for brevity. Logically, the current version is rather similar to the previous version, though it only consumes from one single queue (application.delta_data) to update the states in an Application object.&lt;/p&gt;
&lt;p&gt;After the minor refactoring to the state management, we shift our attention to the main application loop.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;async def main_loop(
    application_setup: Awaitable[Application],
) -&amp;gt; None:
    pygame.init()

    application = await application_setup

    pygame.event.post(pygame.event.Event(SystemEvent.INIT.value))

    tasks = []

    tasks.append(
        asyncio.create_task(
            display_update(
                application.delta_screen,
                application.clock,
            )
        )
    )
    tasks.append(
        asyncio.create_task(events_process(application))
    )

    await application.exit_event.wait()

    pygame.quit()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It is almost similar to the original run function in our original code, with just 2 small changes. Remember I mentioned about wanting a window.onload like system events? Here we are emitting an INIT event before we start the display update and event dispatching loops. Additionally, setup code is passed in as &lt;a rel=&quot;nofollow&quot; href=&quot;https://medium.com/p/ea7ed54ec705/edit&quot;&gt;an&lt;/a&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://medium.com/p/ea7ed54ec705/edit&quot;&gt;Awaitable object&lt;/a&gt;, that is only scheduled to run after pygame.init().&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.to/jeffrey04/understanding-awaitables-coroutines-tasks-and-futures-in-python-gk7&quot;&gt;Understanding Awaitables: Coroutines, Tasks, and Futures in Python&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;That’s practically it, though extracting them out to another module, is a pain due to how these functions reference numerous dataclasses and other helpers. Alongside these changes, I also added some helpers to deal with in-game state management. Spiritually, the design of these helper functions loosely follows &lt;a rel=&quot;nofollow&quot; href=&quot;https://redux.js.org/&quot;&gt;Redux&lt;/a&gt;, though the implementation is a lot simpler in my implementation. We shall see them in action when we go through the game implementation next.&lt;/p&gt;
&lt;h4 id=&quot;framework-in-action-bringing-tic-tac-toe-to-life&quot;&gt;Framework in Action: Bringing Tic-Tac-Toe to Life&lt;/h4&gt;
&lt;p&gt;Framework is now done, what’s next?&lt;/p&gt;
&lt;p&gt;What’s a better way to test the framework, than building a game for real? Now that we delegate the orchestration work to another module, we can now focus purely on the game logic. Throughout the discussion, we can observe how the framework design enables building of a simple game with just dispatching events and implementing the handlers. Game state management as mentioned earlier will be discussed as well.&lt;/p&gt;
&lt;p&gt;Firstly, let’s set-up the game:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;async def setup() -&amp;gt; Application:
    application = Application(screen=pygame.display.set_mode((300, 300)))
    application = await add_event_listeners(
        application,
        (
            (SystemEvent.INIT.value, handle_init),
            (CustomEvent.RESET.value, handle_reset),
            (CustomEvent.MAP_UPDATE.value, handle_update),
            (CustomEvent.JUDGE.value, handle_judge),
        ),
    )

    return application
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A new helper add_event_listeners was written for adding multiple event listeners, though they work similarly as the add_event_listener we discussed last week. In case you are wondering at this point, yes, this is all we needed to implement our tic-tac-toe game. Since we split out the orchestration code to another module, our run function now becomes:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from core import main_loop

async def run() -&amp;gt; None:
    await main_loop(setup())
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Remember main_loop() takes an Awaitable as argument? This is the reason why setup() was written as a coroutine function, and passed into main_loop function without the await keyword.&lt;/p&gt;
&lt;p&gt;Before we start the display update and event dispatching cycles, let’s initialize the game by drawing relevant elements to the application screen object:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;async def handle_init(
    _event: pygame.event.Event, target: Application, logger: BoundLogger, **detail: Any
):
    for row, col in product(range(3), range(3)):
        element = await box_draw(target, row, col, 100)
        element = await add_event_listener(
            element,
            pygame.MOUSEBUTTONDOWN,
            handle_box_click,
        )
        await target.delta_data.put(DeltaAdd(ApplicationDataField.ELEMENTS, element))
        await screen_update(target, element) # type: ignore

    pygame.event.post(pygame.event.Event(CustomEvent.RESET.value))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;During the initialization phase, the handler draws the 3 x 3 tiles to the screen via box_draw, and then registers a mouse click event to each of them. Each tile is then sent to 2 queues, one to populate the application.elements tuple (via target.delta_data.put), and another to update the display (via screen_update). Once done, the handler invokes the RESET event to start a new game.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;async def handle_reset(
    _event: pygame.event.Event, target: Application, logger: BoundLogger, **detail: Any
):
    await state_merge(target, &quot;board&quot;, winner=None, state=GameState.RING, board=None)

    for element in target.elements.values():
        delta = await box_redraw(target, (255, 255, 255), element, Symbol.EMPTY) # type: ignore
        await target.delta_data.put(
            DeltaUpdate(ApplicationDataField.ELEMENTS, element, delta)
        )
        await screen_update(target, delta)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As the game is resetting, it first reinitializes the game state via state_merge.The helper sends a DeltaOperation to update application.state to&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
    &quot;board&quot;: {
        &quot;winner&quot;: None,
        &quot;state&quot;: GameState.END,
        &quot;board&quot;: None
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Elements in the game board also get redrawn (via box_redraw) to ensure they do not contain a ring or cross symbol. As we did earlier, all of them are then sent to the queues for application and display update.&lt;/p&gt;
&lt;p&gt;Our game is now accepting input. And once a click is registered, the registered event handler is called.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;async def handle_box_click(
    _event: pygame.event.Event,
    target: Box,
    application: Application,
    logger: BoundLogger,
    **detail: Any,
) -&amp;gt; None:
    element = None

    match state_get(application, &quot;board&quot;).get(&quot;state&quot;):
        case GameState.END:
            await logger.ainfo(&quot;END&quot;)
            pygame.event.post(pygame.event.Event(CustomEvent.RESET.value))

        case GameState.TIE:
            await logger.ainfo(&quot;TIE&quot;)
            pygame.event.post(pygame.event.Event(CustomEvent.RESET.value))

        case GameState.RING if target.value == Symbol.EMPTY:
            element = await box_update(
                application,
                target,
                Symbol.RING,
            )
            await state_merge(application, &quot;board&quot;, state=GameState.CROSS)

        case GameState.CROSS if target.value == Symbol.EMPTY:
            element = await box_update(
                application,
                target,
                Symbol.CROSS,
            )
            await state_merge(application, &quot;board&quot;, state=GameState.RING)

        case _:
            await logger.aerror(&quot;Invalid click&quot;)

    if element:
        pygame.event.post(pygame.event.Event(CustomEvent.MAP_UPDATE.value))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Click events are handled differently, depending on the state of the board. If there is no valid moves available, in the events of an ended or tie game, then we log a message, and emit a RESET event. Otherwise, we update the clicked element to the symbol corresponding to the current state of the board, followed by a board state change for the next symbol. Lastly, we trigger a MAP_UPDATE event to update the current mapping of the board for judging.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@dataclass
class BoardMap:
    mapping: dict[tuple[int, int], Symbol]

async def handle_update(
    _event: pygame.event.Event, target: Application, logger: BoundLogger, **detail: Any
):
    await state_merge(
        target,
        &quot;board&quot;,
        map=BoardMap(
            {
                (item.column, item.row): item.value
                for item in target.elements.values()
                if isinstance(item, Box)
            }
        ),
    )

    pygame.event.post(pygame.event.Event(CustomEvent.JUDGE.value))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;BoardMap is just a dataclass keeping the current state of the board, what symbol is placed at each tile. As soon as it is updated, a JUDGE event is triggered to evaluate the game.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;WIN_LINES = [
    # Rows
    {(0, 0), (0, 1), (0, 2)},
    {(1, 0), (1, 1), (1, 2)},
    {(2, 0), (2, 1), (2, 2)},
    # Columns
    {(0, 0), (1, 0), (2, 0)},
    {(0, 1), (1, 1), (2, 1)},
    {(0, 2), (1, 2), (2, 2)},
    # Diagonals
    {(0, 0), (1, 1), (2, 2)},
    {(0, 2), (1, 1), (2, 0)},
]

async def handle_judge(
    _event: pygame.event.Event, target: Application, logger: BoundLogger, **detail: Any
):
    if not (bmap := state_get(target, &quot;board&quot;).get(&quot;map&quot;)):
        return

    tiles, winner = (), None

    for symbol in (Symbol.RING, Symbol.CROSS):
        places = {coor for coor, item in bmap.mapping.items() if symbol == item}

        for line in WIN_LINES:
            if line.issubset(places):
                winner = symbol
                tiles = line
                break

        if winner:
            break

    if winner:
        await logger.ainfo(&quot;Winner is found&quot;, winner=winner)
        await state_merge(target, &quot;board&quot;, winner=winner, state=GameState.END)

        for element in target.elements.values():
            if (element.column, element.row) in tiles: # type: ignore
                delta = await box_redraw(target, (255, 255, 0), element, winner) # type: ignore
                await target.delta_data.put(
                    DeltaUpdate(ApplicationDataField.ELEMENTS, element, delta)
                )
                await screen_update(
                    target,
                    delta, # type: ignore
                )
    elif (
        len([symbol for _, symbol in bmap.mapping.items() if symbol == Symbol.EMPTY])
        == 0
    ):
        await logger.ainfo(&quot;End with a tie&quot;)
        await state_merge(target, &quot;board&quot;, state=GameState.TIE)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Not the most robust code I would proudly print on a t-shirt and parade with it. Essentially, it judges the game to find the winning line and symbol. Redrawing the winning tiles will take place whenever applicable. Besides that, the relevant state of the board is also queued for update (either an END or TIE) whenever needed.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn-images-1.medium.com/max/1024/1*9hZL4Bm3asPWiieXpsof8Q.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;By utilizing events and state_merge strategically, the simple game of tic-tac-toe is implemented. All the event dispatching work and display update are completely handled by the framework itself. All we needed to do, when changes occur, is to submit the change to the relevant queue. While there are certainly potential improvements to be made, as a weekend project I deem it good enough.&lt;/p&gt;
&lt;h4 id=&quot;beyond-tic-tac-toe-lessons-learned-and-what-s-next&quot;&gt;Beyond Tic-Tac-Toe: Lessons Learned and What’s Next&lt;/h4&gt;
&lt;p&gt;Can we build more things with it?&lt;/p&gt;
&lt;p&gt;Short answer to the question is yes, but I don’t see much point in doing so for most people. Instead, we should reflect on what we learned so far. What is the purpose of the project? What have we achieved so far? What do we do with this piece of code?&lt;/p&gt;
&lt;p&gt;Extracting the orchestration code into a framework is a totally unexpected outcome. Yet, I am happy with the turns of events leading to what we have now. With some polish, the resulting framework should be able to be applied for other simple graphical applications. Personally, I am quite curious on seeing how the framework scales with more elements on screen.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn-images-1.medium.com/max/1024/0*fEbkPFllr5iRzZ_h&quot; alt=&quot;&quot;&gt;&lt;br&gt;
&lt;em&gt;Photo by Dan Freeman on Unsplash&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;As an experimental project, it certainly is quite rough on the edges. Much thought and polish are still needed in the framework API design for consistency. There are also some experimental code snippets that are not covered in the article as they are irrelevant to the game featured this week. Some of the most interesting helpers I can’t wait to try out are the functions inspired by setTimeout and element.dispatchEvent().&lt;/p&gt;
&lt;p&gt;Perhaps a re-implementation of &lt;a rel=&quot;nofollow&quot; href=&quot;https://kitfucoda.medium.com/code-rains-revelation-embracing-existence-before-perfection-60f5c641963a&quot;&gt;the code rain animation a-la the Matrix&lt;/a&gt;?&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.to/jeffrey04/code-rains-revelation-embracing-existence-before-perfection-2mnc&quot;&gt;Code Rain’s Revelation: Embracing Existence Before Perfection&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Now, back to the original question asked earlier, at the beginning of this section. The framework is proven capable of building a simple game, as shown in this article. Despite that, do go for a proper game engine like Godot for serious projects. Treat the work presented here as a personal passion project, though I am open for feedback and collaboration. Seriously, this exploration makes me appreciate how game engines do all the heavy-lifting work.&lt;/p&gt;
&lt;p&gt;Finally, thanks for reading this far. If you are interested in checking out the project, it is still hosted on GitHub at &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/Jeffrey04/pygame-ttt/&quot;&gt;the same repository&lt;/a&gt;. Though I cannot commit to a fixed publication schedule due to my new job, I shall try writing again soon.&lt;/p&gt;
&lt;p&gt;A quick note on how this article came together. While Gemini helped with the drafting and refining of the language, all the core ideas, the technical insights, and every line of code you’ve seen are entirely mine. This project truly reflects my personal journey and passion for game development. If you enjoyed this deep dive and want to follow more of my explorations into coding and game development, consider subscribing or following me on &lt;a rel=&quot;nofollow&quot; href=&quot;https://kitfucoda.medium.com/&quot;&gt;Medium&lt;/a&gt;. Your support means a lot!&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/3729/the-pygame-framework-i-didnt-plan-building-tic-tac-toe-with-asyncio-and-events</guid>
<pubDate>Sat, 05 Jul 2025 13:46:49 +0000</pubDate>
</item>
<item>
<title>Concurrency vs. Parallelism: Achieving Scalability with ProcessPoolExecutor</title>
<link>https://coderlegion.com/1822/concurrency-vs-parallelism-achieving-scalability-with-processpoolexecutor</link>
<description>&lt;p&gt;In our previous exploration, we dove deep into AsyncIO by &lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.to/jeffrey04/asyncio-task-management-a-hands-on-scheduler-project-2e54&quot;&gt;writing a task scheduler&lt;/a&gt;. We discovered how IO-bound operations were scheduled to the event loop concurrently while building our task scheduler project. Building on what we’ve done, we will revisit the project and scale it to run CPU-bound operations.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn-images-1.medium.com/max/1024/1*sGUALoy-YdOG_beDjVYm0A.jpeg&quot; alt=&quot;&quot;&gt;&lt;br&gt;
&lt;em&gt;More chefs, more dishes, illustration generated by Microsoft Copilot&lt;/em&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.to/jeffrey04/how-to-write-an-asyncio-telegram-bot-in-python-4hig&quot;&gt;How to write an AsyncIO Telegram bot in Python&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.to/jeffrey04/understanding-awaitables-coroutines-tasks-and-futures-in-python-gk7&quot;&gt;Understanding Awaitables: Coroutines, Tasks, and Futures in Python&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.to/jeffrey04/asyncio-task-management-a-hands-on-scheduler-project-2e54&quot;&gt;AsyncIO Task Management: A Hands-On Scheduler Project&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Concurrency vs. Parallelism: Achieving Scalability with ProcessPoolExecutor&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 id=&quot;asyncio-a-foundation-for-concurrency&quot;&gt;AsyncIO: A Foundation for Concurrency&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn-images-1.medium.com/max/1024/0*mKYnSI3lcyvVJ3xd&quot; alt=&quot;&quot;&gt;&lt;br&gt;
&lt;em&gt;Photo by John Barkiple on Unsplash&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;It is hard to avoid talking about concurrency in discussions about asynchronous programming. However, when it gets mentioned, often times it gets mixed up with parallelism. When Golang came out, people raved about how goroutines are efficient, and that was the first time I hear people discussing concurrency after my undergrad days. Rob Pike eventually &lt;a rel=&quot;nofollow&quot; href=&quot;https://go.dev/blog/waza-talk&quot;&gt;delivered a talk&lt;/a&gt; aiming to distinguish the two concepts and it became quite popular among my peers.&lt;/p&gt;
&lt;p&gt;While building our project last week, I mentioned event loops functions like a timetable where we slot in &lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.to/jeffrey04/understanding-awaitables-coroutines-tasks-and-futures-in-python-gk7&quot;&gt;awaitables&lt;/a&gt; for execution. They would be scheduled to run one at a time, switching to another whenever one is done, or is busy doing IO-operations. For example, when we send multiple dex tasks, it would attempt to (order is not necessarily exact, just to illustrate the idea)&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Start an HTTP request to the PokeAPI&lt;/li&gt;
&lt;li&gt;Wait for the response for (1)&lt;/li&gt;
&lt;li&gt;Start another HTTP request to the PokeAPI&lt;/li&gt;
&lt;li&gt;Wait for the response for (3)&lt;/li&gt;
&lt;li&gt;Continue the task started in (1), and compute the result&lt;/li&gt;
&lt;li&gt;Start another HTTP request to the PokeAPI&lt;/li&gt;
&lt;li&gt;Wait for the response for (6)&lt;/li&gt;
&lt;li&gt;Continue the task started in (3), and compute the result&lt;/li&gt;
&lt;li&gt;Continue the task started in (6), and compute the result&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;We can see the event loop improves the efficiency by keeping the CPU busy, while IO is happening in the background. This is an example of concurrency, in Pike’s word: dealing with a lot of things at once. In a way, it is like planning our time to minimize time waste on waiting in the kitchen. For example, we can proceed to cut ingredients for other dishes, while waiting for our pot of soup to boil. This allows us to concurrently prepare or cook multiple dishes at once.&lt;/p&gt;
&lt;p&gt;So we now know we can achieve concurrency with AsyncIO for IO-bound operations. How about CPU-bound operations?&lt;/p&gt;
&lt;p&gt;We are fortunate to live in an era where computing devices sports multicore CPU by default. Theoretically this should improve efficiency, but spreading work to multiple cores is a challenge. Continuing our cooking analogy, having an extra core or CPU is like having another kitchen and chef. Both chefs can now work together and get more dishes prepared at once. They can both choose to schedule their time wisely, to work concurrently as well.&lt;/p&gt;
&lt;p&gt;Go’s goroutines can be scheduled to both run concurrently, or to be distributed to multiple CPU cores to run at once. In Python, we can achieve concurrency with AsyncIO. However, for parallelism, we would have to use tools like ProcessPoolExecutor in addition to AsyncIO. Let’s get started by continuing our example project last week by making fib run in parallel.&lt;/p&gt;
&lt;h4 id=&quot;harnessing-parallelism-processpoolexecutor-in-action&quot;&gt;Harnessing Parallelism: ProcessPoolExecutor in Action&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn-images-1.medium.com/max/1024/0*y2IwwK-0f0okYcLN&quot; alt=&quot;&quot;&gt;&lt;br&gt;
&lt;em&gt;Photo by Becca Tapert on Unsplash&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;We usually opt to spawn a new thread, when we want to distribute work across the available cores. With threads, data can be shared among the main process and the spawn threads. However, it also means a race condition can occur when multiple threads are writing to the same variable. Hence, the &lt;a rel=&quot;nofollow&quot; href=&quot;https://wiki.python.org/moin/GlobalInterpreterLock&quot;&gt;global interpreter lock (GIL)&lt;/a&gt; was introduced, to limit the behaviour. While it protects the shared data, it makes threads inefficient as it depends on the lock.&lt;/p&gt;
&lt;p&gt;Ensuring data is properly shared across processes and threads is therefore one of the biggest challenge in parallelism. There are ongoing efforts to deal with the limitations, or even &lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.python.org/3/howto/free-threading-python.html&quot;&gt;removing GIL entirely in recent releases&lt;/a&gt;. But for now, most of the time we would just start new processes to distribute works to be done at once. In our case, let’s set up ProcessPoolExecutor in our repl function:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from concurrent.futures import ProcessPoolExecutor

async def repl() -&amp;gt; None:
    with (
        ProcessPoolExecutor() as executor,
        suppress(asyncio.CancelledError),
    ):
        ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Previously, in our evaluator, we would spawn a new thread whenever we work on fib, through the use of asyncio.to_thread. We also know it is just an abstraction of loop.run_in_executor. Now that we are using our own process executor, let’s revert to use run_in_executor, with our own ProcessPoolExecutor&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;async def evaluate(…):
    match expression:
        ...

        case Expression(command=Command.FIB):
            return asyncio.get_running_loop().run_in_executor(
                executor, fib, expression.args[0]
            )
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now, whenever we issue a fib command, a new process is spawn instead of a thread to execute the (potentially) super slow CPU-bound operation. Starting a new process is considered more expensive, as we are pending resource allocation from the operating system. Sharing data and synchronization between processes also now require more effort, and we will discuss further about this later.&lt;/p&gt;
&lt;p&gt;The loop.run_in_executor is essentially an abstraction of executor.submit call, where it takes function and the corresponding parameters. Therefore, in general, we would write it as&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;with ProcessPoolExecutor() as executor:
    future = executor.submit(fib, 100)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The returned future object, has a nearly identical design compared to the asyncio counterpart, sans the asynchronous magic.&lt;/p&gt;
&lt;p&gt;While we are at it, let’s add a new command to send multiple fib operations, where the format is fib-multi  [ …]. For instance, when we do fib-multi 100 200 300, it would schedule fib 100, fib 200 and fib 300 to run in parallel. For that, we add a new case in our evaluator&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; async def evaluate(...):
    match expression:
        ...

        case Expression(command=Command.FIB_MULTI):
            return asyncio.gather(
                *[
                    asyncio.get_running_loop().run_in_executor(
                        executor, fib2, nth
                    )
                    for nth in expression.args
                ]
            ) 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When we use asyncio.gather, it takes all the awaitables passed as parameters and return a Future object. The result of the awaitables will be returned as a list when .result() is called. In our case, since we are setting the executor explicitly to a ProcessPoolExecutor, the futures are then scheduled to run in parallel.&lt;/p&gt;
&lt;p&gt;And for that, we need to revise our job and dash to properly display the result, namely to handle cases where the result returned is a list.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def job(
    task: tuple[
        str,
        Event | None,
        asyncio.Task[str] | asyncio.Future[str] | asyncio.Future[list[str]],
    ],
) -&amp;gt; str:
    try:
        match task[-1]:
            case asyncio.Future():
                return (
                    &quot;; &quot;.join(task[-1].result())
                    if isinstance(task[-1].result(), list)
                    else task[-1].result()
                ) # type: ignore

            case _:
                return &quot;Still waiting&quot;

    except asyncio.InvalidStateError:
        return &quot;Still waiting&quot;

def dash(
    tasks: Sequence[
        tuple[
            str,
            Event | None,
            asyncio.Task[str] | asyncio.Future[str] | asyncio.Future[list[str]],
        ]
    ],
) -&amp;gt; str:
    result = [
        &quot;\t&quot;.join((&quot;id&quot;, f&quot;{'command':16s}&quot;, &quot;done?&quot;, &quot;result&quot;)),
        &quot;\t&quot;.join((&quot;==&quot;, f&quot;{'=' * 16}&quot;, &quot;=====&quot;, &quot;======&quot;)),
    ]

    for num, (line, _, task) in enumerate(tasks, 1):
        task_done, task_result = False, None

        if task_done := task.done():
            task_result = (
                &quot;; &quot;.join(task.result())
                if isinstance(task.result(), list)
                else task.result()
            )

        result.append(
            &quot;\t&quot;.join(
                (
                    str(num),
                    f&quot;{line:16s}&quot;,
                    str(task_done),
                    task_done and str(task_result)[:16] or &quot;&quot;,
                )
            )
        )

    return &quot;\n&quot;.join(result)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We can now issue an actual fib-multi command, and check our system monitor to see the operating system does indeed allocate multiple processes to do the job.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn-images-1.medium.com/max/1024/1*CFW4N52aCixLjxXe6fDjjg.png&quot; alt=&quot;&quot;&gt;&lt;br&gt;
&lt;em&gt;A screen-capture from my system monitor&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;On the other hand, dex is still unaffected, and would continue to work concurrently despite the changes introduced to fib. This exercise show how it is possible to introduce parallelism into a concurrent program.&lt;/p&gt;
&lt;h4 id=&quot;managing-processes-cancellation-and-communication&quot;&gt;Managing Processes: Cancellation and Communication&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn-images-1.medium.com/max/1024/0*82eJBcJ8GOU9bg7R&quot; alt=&quot;&quot;&gt;&lt;br&gt;
&lt;em&gt;Photo by Mark Boss on Unsplash&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;A big drawback of starting a new process is that communication takes a lot of effort, and we lose some control over it. For instance, if we create a task in asyncio, we can cancel the task whenever needed with .cancel(). However, despite the ProcessPoolExecutor provides a .cancel() method as well, it does not always work for scheduled futures that are in progress.&lt;/p&gt;
&lt;p&gt;One way to workaround the problem is through event objects. In our example, we will need 2 types of events, one for task cancellation, and another for application exit. Let’s get fib to subscribe to both of them.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def fib(nth, exit_event=None, cancel_event=None):
    ...

    for i in range(1, nth + 1):
        if (exit_event and exit_event.is_set()) or (
            cancel_event and cancel_event.is_set()
        ):
            return &quot;Cancelled&quot;

        ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The key is to stop the loop, whenever either of the events is set. In our case, would be either user is exiting the program with quit, or cancelling the job with kill . Now we move on to the evaluator to ensure both these events are sent to fib.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from multiprocessing.managers import SyncManager

async def evaluate(
    …
    executor: ProcessPoolExecutor,
    exit_event: Event,
    manager: SyncManager,
    expression: Expression,
):
    match expression:
        ...

        case Expression(command=Command.FIB):
            cancel_event = manager.Event()
            return cancel_event, asyncio.get_running_loop().run_in_executor(
                executor, fib, expression.args[0], exit_event, cancel_event
        )

        case Expression(command=Command.FIB_MULTI):
            cancel_event = manager.Event()
            return cancel_event, asyncio.gather(
                *[
                    asyncio.get_running_loop().run_in_executor(
                        executor, fib, nth, exit_event, cancel_event
                    )
                    for nth in expression.args
                ]
            )
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The application exit_event is going to be applicable for all fib operations, so we are requiring it in the parameters. On the other hand, a cancel_event is only relevant for a particular job, hence it is created just for it. Then we set up the global exit_event in repl().&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import multiprocessing

async def repl() -&amp;gt; None:
    with (
        ProcessPoolExecutor(max_workers=5) as executor,
        suppress(asyncio.CancelledError),
    ):
        manager = multiprocessing.Manager()
        exit_event = manager.Event()
        …
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We create Event() object through the multiprocessing.Manager() object because it is generally more reliable, especially we are passing the object between processes.&lt;/p&gt;
&lt;p&gt;The ShutdownHandler would require a little bit of revision so it is aware of the exit_event.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@dataclass
class ShutdownHandler:
    exit_event: Event
    signal: int | None = None

    async def __call__ (self) -&amp;gt; None:
        logger.info(&quot;Shutting down tasks. signal:%s&quot;, self.signal or &quot;&quot;)

        self.exit_event.set()

        ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now we can prompt fib, which is our CPU-bound task to return when the application exits. The changes in the handler, also require init() function to take exit_event as parameter&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def init(exit_event: Event) -&amp;gt; None:
    loop = asyncio.get_running_loop()

    for sig in (signal.SIGTERM, signal.SIGHUP, signal.SIGINT):
        handler = ShutdownHandler(exit_event, sig)
        loop.add_signal_handler(sig, lambda: asyncio.create_task(handler()))

    loop.set_exception_handler(ExceptionHandler(ShutdownHandler(exit_event)))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now the application exit part is done, let’s move on to task cancellation. In the evaluator, we see the CPU-bound jobs also return the cancel_event, so we need to properly store them in our list of tasks in repl()&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;async def repl() -&amp;gt; None:
    ...

                match result := await thread_last(
                    line,
                    (tokenize, t),
                    (parse, g),
                    (
                        evaluate,
                        client,
                        executor,
                        exit_event,
                        manager,
                        tasks,
                        ShutdownHandler(exit_event),
                    ),
                ):
                    case (_, asyncio.Future()):
                        tasks.append((line, *result))
                        print(f&quot;Task {len(tasks)} is submitted&quot;)

                    case asyncio.Task():
                        tasks.append((line, None, result))
                        print(f&quot;Task {len(tasks)} is submitted&quot;)

                    case _:
                        print(result)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And the corresponding kill function is now&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def kill(task):
    match task[-1]:
        case asyncio.Task():
            task[-1].cancel()

        case asyncio.Future():
            assert task[1]

            task[1].set()

    return f'Task &quot;{task[0]}&quot; is killed'
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Note that asyncio.Task is a subclass of asyncio.Future, so the case order matters.&lt;/p&gt;
&lt;h4 id=&quot;the-power-of-the-hybrid-approach&quot;&gt;The Power of the Hybrid Approach&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn-images-1.medium.com/max/1024/0*sr-mylPImpHPq_Tx&quot; alt=&quot;&quot;&gt;&lt;br&gt;
&lt;em&gt;Photo by Elena Mozhvilo on Unsplash&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;In our example, we can use ProcessPoolExecutor to introduce parallelism to our application. This allows us to scale CPU-bound operations so that they can be distributed to other cores of the CPU, allowing them to be executed simultaneously. On the other hand, IO-bound operations are still scheduled to run concurrently.&lt;/p&gt;
&lt;p&gt;Remember the cooking analogy we mentioned earlier? Doing things concurrently is very similar to cooking. It is possible to prepare multiple dishes at a time with clever time management. While we wait for our chicken in the oven, we can go stir-fry whatever that is in the pot.&lt;/p&gt;
&lt;p&gt;On the other hand, doing it in parallel means adding a new chef (and maybe even another kitchen). Adding a new chef, would definitely increase throughput, allowing more dishes to be done at once. On the other hand, either or both of them can still be working concurrently, increasing efficiency. In our case, it would be starting separate processes, and each manages their own event loop to take on tasks.&lt;/p&gt;
&lt;p&gt;I hope I am doing a good enough job in this tour of AsyncIO and asynchronous programming. I certainly learned a lot throughout these few weeks, revisiting the documentation, and through building the example project. The updated project can still be &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/Jeffrey04/asyncio-post&quot;&gt;accessed on GitHub&lt;/a&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/Jeffrey04/asyncio-post&quot;&gt;,&lt;/a&gt;,) with week1 and week2 code separated, allowing comparison. Thank you again for your time, and I shall write again, next week.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;This article was enhanced by the editorial assistance of a large language model, which provided valuable feedback on clarity, flow, and adherence to writing principles. While the core ideas and code remain my own, the LLM’s suggestions significantly contributed to the overall quality and readability of the piece. I am always open to collaboration on interesting projects and exploring job opportunities. Feel free to connect with me &lt;a rel=&quot;nofollow&quot; href=&quot;https://kitfucoda.medium.com/&quot;&gt;here on Medium&lt;/a&gt; or via &lt;a rel=&quot;nofollow&quot; href=&quot;https://linkedin.com/in/jeffrey04&quot;&gt;my LinkedIn profile&lt;/a&gt; to discuss further.&lt;/p&gt;
</description>
<category>Adv Concepts</category>
<guid isPermaLink="true">https://coderlegion.com/1822/concurrency-vs-parallelism-achieving-scalability-with-processpoolexecutor</guid>
<pubDate>Tue, 08 Apr 2025 14:57:43 +0000</pubDate>
</item>
<item>
<title>AsyncIO Task Management: A Hands-On Scheduler Project</title>
<link>https://coderlegion.com/1756/asyncio-task-management-a-hands-on-scheduler-project</link>
<description>&lt;p&gt;We discussed Awaitables &lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.to/jeffrey04/understanding-awaitables-coroutines-tasks-and-futures-in-python-gk7&quot;&gt;last week&lt;/a&gt;. The article covered coroutines, tasks and futures as well as a quick introduction to the event loop. Let’s build an example task management project to continue our learning journey.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn-images-1.medium.com/max/1024/1*LlAB5imhzAROhennWMDxKg.jpeg&quot; alt=&quot;&quot;&gt;&lt;br&gt;
&lt;em&gt;A cute cartoon generated by Microsoft Copilot on the topic&lt;/em&gt;&lt;/p&gt;
&lt;h4 id=&quot;introduction-building-our-task-scheduler&quot;&gt;&lt;strong&gt;Introduction: Building Our Task Scheduler&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;In the previous article, we mentioned briefly on event loops. Think of it as a timetable-like structure, which is executed throughout the lifetime of an application. Awaitables are then scheduled for concurrent execution within it. In recent releases, the Python developers provided more abstractions and reduced the need to manually manage event loops.&lt;/p&gt;
&lt;p&gt;In the article on &lt;a rel=&quot;nofollow&quot; href=&quot;https://kitfucoda.medium.com/writing-a-telegram-bot-in-python-866972ab63f5&quot;&gt;programming a chatbot&lt;/a&gt;, we created a new process each for the chatbot and the web application. We then call asyncio.run in each of the processes to execute the first coroutine. So let’s copy that to bootstrap our sample application:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import asyncio

async def main():
    await asyncio.sleep(5)

if __name__ == ' __main__':
    asyncio.run(main()) 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If we run the program, it will run for 5 seconds through asyncio.sleep, and then exits. As mentioned last week, calling a coroutine (asyncio.sleep) would not cause it to execute. Omitting the await keyword will result in an error. This is because the coroutine object returned by asyncio.sleep(5) does not get scheduled for execution.&lt;/p&gt;
&lt;p&gt;In the snippet, calling asyncio.run creates an event loop, schedule the coroutine (main()) for execution, and then perform clean-up after completion.&lt;/p&gt;
&lt;p&gt;We are building a task management system, where we submit tasks, and then run a check for result manually. So the first thing we need is a simple user interface. I like the simplicity of repl-like (read-evaluate-parse-loop) interface, so we build one. To interact with our application, users can issue simple commands. We start with 2 commands below, by adapting from the implementation we did last week.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;dex : fetch the name of a Pokémon from the Pokédex based on the number &lt;/li&gt;
&lt;li&gt;fib : Returns the th Fibonacci number&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Let’s start by implementing dex and fib, each represents an I/O bound and CPU-bound operation respectively.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;async def dex(client: httpx.AsyncClient, id: int) -&amp;gt; str:
    assert isinstance(id, int)

    response = await client.get(f&quot;https://pokeapi.co/api/v2/pokemon/{id}/&quot;)

    return f&quot;The pokemon with id {id} is {response.json()['name']}&quot;

def fib(nth: int) -&amp;gt; str:
    assert isinstance(nth, int) and nth &amp;gt; 0

    result = ()

    for i in range(1, nth + 1):
        match i:
            case 1:
                result = (0,)

            case 2:
                result += (1,)

            case _:
                result = result[1:] + (sum(result),)

    assert len(result) &amp;gt; 0

    return f&quot;The {nth}th fibonacci number is {result[-1]}&quot; 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For dex, we want to reuse the httpx.AsyncClient, so we adapt our code to require it in the arguments.&lt;/p&gt;
&lt;h4 id=&quot;tasks-and-futures-in-action&quot;&gt;&lt;strong&gt;Tasks and Futures in Action&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;Assembling the repl-like interface would need a little bit of work. We need to be able to parse the command entered by our users and schedule the correct task. For this, we use funcparserlib as featured in a &lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.to/jeffrey04/how-to-parse-computer-code-advent-of-code-2024-day-3-2g84&quot;&gt;previous Advent of Code article&lt;/a&gt;. We are skipping the implementation detail for parser and lexer in this article, and only focus on the evaluation part, as shown below:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;async def evaluate(
    client: httpx.AsyncClient,
    expression: Expression,
) -&amp;gt; Awaitable[str] | str:
match expression:
    case Expression(command=Command.DEX):
        return asyncio.create_task(dex(client, expression.args[0]))

    case Expression(command=Command.FIB):
        return asyncio.create_task(asyncio.to_thread(fib, expression.args[0]))

    case _:
        raise Exception(&quot;Unknown command&quot;) 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We want to enable users to check the progress of tasks later, so we return them as tasks. For dex requests, it is implemented as a coroutine, hence we just wrap it as a task, and return it. However, for fib, as it is CPU-bound, it will not benefit much from asynchronous execution. As discussed last week, we need to turn it into a future, so it can be executed as a thread of process, as shown below&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;loop = asyncio.get_running_loop()

# get the 100th Fibonacci number
future = loop.run_in_executor(None, fib, 100)

# later in the execution
result = await future 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As part of the abstraction effort, asyncio.get_running_loop() will fetch the event loop created by asyncio.run we called earlier. However, if the code snippet is executed after the main() coroutine is completed, it will result in an error. The first argument in run_in_executor expects either a ProcessPoolExecutor, or a ThreadPoolExecutor, but will use a default ThreadPoolExecutor if None is passed. For most cases, it is sufficient to just use the default, and hence we can turn it into a task instead&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fib_coro = asyncio.to_thread(fib, 100)

task = asyncio.create_task(fib_coro)

# later
result = await task
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now we have the executor done, let’s assemble the repl. We use prompt-toolkit to prompt the user for command, and then it does the following operations in sequence&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Read the user entered command and arguments&lt;/li&gt;
&lt;li&gt;Parse the command and arguments, and return an Expression object&lt;/li&gt;
&lt;li&gt;Evaluate the expression object, and schedule tasks with asyncio.create_task&lt;/li&gt;
&lt;li&gt;Loop back to step 1&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The main coroutine, is now written as follows, with the corresponding steps are annotated as comments in the snippet&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;async def main() -&amp;gt; None:
    t, g = TOKENIZER, grammar()
    session = PromptSession()

    tasks = []

    async with httpx.AsyncClient() as client:
        # step 4: loop
        while True:
            # step 1: read input
            line = await session.prompt_async(&quot;&amp;gt; &quot;, handle_sigint=False)

            match result := await thread_last(
                line,
                # step 2a: tokenize the input
                (tokenize, t),
                # step 2b: parse the input
                (parse, g),
                # step 3: evaluate
                (evaluate, client),
            ):
                case asyncio.Task():
                    tasks.append((line, result))
                    print(f&quot;Task {len(tasks)} is submitted&quot;)

                case _:
                    print(result)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Some notes on the snippet:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;t is for the definition of tokens (e.g, a sequence of letters is the name of a command)&lt;/li&gt;
&lt;li&gt;g is for the definition of the commands (what dex and fib means)&lt;/li&gt;
&lt;li&gt;The thread_last call in the snippet is taken from toolz. What it does is simply evaluate(client, parse(g, tokenize(t, line))) but we keep it that way to aid readability.&lt;/li&gt;
&lt;li&gt;We collect the tasks after evaluations into a simple list, we will work with it later&lt;/li&gt;
&lt;li&gt;In the match statement, we print task ID if the evaluator returns a Task, or the returned string otherwise.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;So let’s recap what we have built so far. We should be able to run the program, and then issue commands to fetch Pokémon names, and calculate Fibonacci numbers. You may have noticed, we have yet to print the result on screen.&lt;/p&gt;
&lt;p&gt;On the other hand, we also know that main() is calling the evaluator to schedule tasks to the event loop with asyncio.create_task(). We also see await is used when the order of execution is crucial:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;When we perform a GET HTTP request, so we can parse the response JSON after receiving it&lt;/li&gt;
&lt;li&gt;When we are receiving a user input, as we need to pass the input to parser later&lt;/li&gt;
&lt;li&gt;We need to wait for evaluator to create a task, before we loop back to receiving another input from the user&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;So what are the differences between the two, when should we use asyncio.create_task() and await? As a rule of thumb, if the order of execution does not matter, or if we only care about the outcome much later, use asyncio.create_task. If the coroutine does something that is needed later as shown above, then use await to ensure it is run as soon as possible, before we move on to the next line in code.&lt;/p&gt;
&lt;h4 id=&quot;managing-task-execution-and-displaying-results&quot;&gt;&lt;strong&gt;Managing Task Execution and Displaying Results&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;Now we can start working on the tracker, and then build a dashboard to list tasks. These are the two command we want to build next:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;job : We show the result of computation, or a message indicating it is still waiting for a result&lt;/li&gt;
&lt;li&gt;dash: We draw a simple table to show some information about all submitted tasks&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Starting with the actual function again, first we have one for job&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def job(task: tuple[str, asyncio.Task[str]]) -&amp;gt; str:
    try:
        return task[-1].result()

    except asyncio.InvalidStateError:
        return &quot;Still waiting&quot; 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Each entry in the task list, is a tuple of user input, and the actual Task object. So in this function, we are interested in finding the result. However, if the task is still not ready, then calling .result() will raise InvalidStateError, then we can return a message to tell users that it is still waiting.&lt;/p&gt;
&lt;p&gt;Then a dashboard to display an overview of the tasks&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def dash(tasks: Sequence[tuple[str, asyncio.Task[str]]]) -&amp;gt; str:
    result = [
        &quot;\t&quot;.join((&quot;id&quot;, f&quot;{'command':16s}&quot;, &quot;done?&quot;, &quot;result&quot;)),
        &quot;\t&quot;.join((&quot;==&quot;, f&quot;{'=' * 16}&quot;, &quot;=====&quot;, &quot;======&quot;)),
    ]

    for num, (line, task) in enumerate(tasks, 1):
        result.append(
            &quot;\t&quot;.join(
                (
                    str(num),
                    f&quot;{line:16s}&quot;,
                    str(task.done()),
                    task.done() and str(task.result())[:16] or &quot;&quot;,
                )
            )
        )

    return &quot;\n&quot;.join(result)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The function simply returns a table-like report to show if tasks are done, and the corresponding result whenever applicable.&lt;/p&gt;
&lt;p&gt;Then we need to expand the evaluator, to call the corresponding job tracking functions we just added above.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;async def evaluate(
    client: httpx.AsyncClient,
    tasks: Sequence[tuple[str, asyncio.Task[str]]],
    expression: Expression,
) -&amp;gt; Awaitable[str] | str:
    match expression:
        ...

        case Expression(command=Command.JOB):
            return job(tasks[expression.args[0] - 1])

        case Expression(command=Command.DASH):
            return dash(tasks)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Previously, every time a user submits a task with either dex or fib, we return a job number. Now they can check the progress with the job command with corresponding job number, or display an overview of submitted jobs with dash.&lt;/p&gt;
&lt;h4 id=&quot;task-cancellation-and-graceful-shutdown&quot;&gt;Task Cancellation and Graceful Shutdown&lt;/h4&gt;
&lt;p&gt;We now already have a mostly complete program. While prompt-toolkit provides some handling for some signals, we can opt to do it on our own too (hence the handle_sigint=False in our session.prompt_async). For that, we start by declaring the ShutdownHandler, to gracefully shut down all scheduled unfinished tasks:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@dataclass
class ShutdownHandler:
    signal: int | None = None

    async def __call__ (self) -&amp;gt; None:
        logger.info(&quot;Shutting down tasks. signal:%s&quot;, self.signal or &quot;&quot;)

        tasks = tuple(
            task for task in asyncio.all_tasks() if task is not asyncio.current_task()
        )

        for task in tasks:
            task.cancel()

        await asyncio.gather(*tasks, return_exceptions=True)

        asyncio.get_running_loop().stop()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We are applying the __call__ dunder &lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.to/jeffrey04/the-versatility-of-call-a-python-developers-secret-weapon-4akd&quot;&gt;discussed previously&lt;/a&gt; to avoid usage of functools.partial. In the coroutine, we are shutting down all the remaining tasks, and then stop the event loop (hence ending the asyncio.run we did in the beginning). Notice we are excluding the current coroutine so asyncio.get_running_loop().stop() will get to run.&lt;/p&gt;
&lt;p&gt;Next, we set up the shutdown handler to run, whenever the program receives certain signals from the operating system. In our example, we are handling SIGTERM, SIGHUP and SIGINT. We can then register the ShutdownHandler we just implemented to the event loop to respond to the signals we listed.&lt;/p&gt;
&lt;p&gt;Now we can proceed to add 2 more commands, namely&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;kill : Kill the job number &lt;/li&gt;
&lt;li&gt;quit: Exit the program&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The corresponding functions for both commands are shown below:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def kill(task: tuple[str, asyncio.Task[str]]) -&amp;gt; str:
    task[-1].cancel()

    return f'Task &quot;{task[0]}&quot; is killed'

def quit(shutdown_handler: Callable[..., Coroutine[None, None, None]]) -&amp;gt; str:
    asyncio.create_task(shutdown_handler())

    return &quot;Exiting&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Every time the method .cancel() is called on a task, a CancelledError is raised. If the task is executing SQL statements in a transaction, this serves as an opportunity to quit the transaction, and rollback to undo changes done so far. But in some cases, we can just ignore the error and quit quietly. For instance, we can just suppress the error in our main() coroutine. While we revisit main(), we need to also ensure init() is called to set up the ShutdownHandler().&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from contextlib import suppress

def init() -&amp;gt; None:
    loop = asyncio.get_running_loop()

    for sig in (signal.SIGTERM, signal.SIGHUP, signal.SIGINT):
        handler = ShutdownHandler(sig)

        loop.add_signal_handler(sig, lambda: asyncio.create_task(handler()))

async def main() -&amp;gt; None:
    with suppress(asyncio.CancelledError):
        init()

        t, g = TOKENIZER, grammar()
        session = PromptSession()

        ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And the updated evaluator function is slightly revised to add support for the two new commands:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;async def evaluate(
    client: httpx.AsyncClient,
    tasks: Sequence[tuple[str, asyncio.Task[str]]],
    shutdown_handler: Callable[..., Coroutine[None, None, None]],
    expression: Expression,
) -&amp;gt; Awaitable[str] | str:
    match expression:
        ...

        case Expression(command=Command.KILL):
            return kill(tasks[expression.args[0]] - 1)

        case Expression(command=Command.QUIT):
            return quit(shutdown_handler)
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;background-tasks-and-event-management&quot;&gt;Background Tasks and Event Management&lt;/h4&gt;
&lt;p&gt;The implementation seems straightforward, as we are constantly interacting with the main() coroutine. It is always waiting for user commands, and by exiting we just terminate the infinite while loop. However, there are cases where the application would just run autonomously. For instance we have a simple application as follows:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;async def background_task():
    while True:
        await asyncio.sleep(10)

        print(&quot;Print something&quot;)

async def main():
    # send work to execute in the background
    asyncio.create_task(background_task())

    # do some work
    ultimate_answer = 42

    # hmm, we are done here

if __name__ == ' __main__':
    asyncio.run(main()) 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A similar pattern can be observed in the article where we discussed chatbot programming, where the background_task is a coroutine to consume messages from a queue. In this mini example program, it will end silently, and the first print() in background_task() will not happen, because main() ended so soon. In the past, I have seen people attaching another infinite loop at the end of main() so background_task() gets to run indefinitely.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;async def main():
    ...

    # let's pretend we are doing work
    while True:
        pass
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;While it seems to work, such that the application seems to continue to run. However, background_task() is not printing because the infinite loop in main() is blocking other coroutines from running. As a workaround, we could replace pass with something like await asyncio.sleep(2), but it felt like a hack and I don’t quite like it.&lt;/p&gt;
&lt;p&gt;Alternatively, we could create an asyncio.Event object, and then pass it to main(). Then we adapt the ShutdownHandler discussed previously, to receive an event object, as shown below:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@dataclass
class ShutdownHandler:
    exit_event: asyncio.Event
    signal: int | None = None

    async def __call__ (self) -&amp;gt; None:
        logger.info(&quot;Shutting down tasks. signal:%s&quot;, self.signal or &quot;&quot;)
        self.exit_event.set()

async def main(exit_event: asyncio.Event):
    loop = asyncio.get_running_loop()

    for sig in (signal.SIGTERM, signal.SIGHUP, signal.SIGINT):
        handler = ShutdownHandler(exit_event, sig)

        loop.add_signal_handler(sig, lambda: asyncio.create_task(handler()))

    # send work to execute in the background
    task = asyncio.create_task(background_task())

    # do some work
    ultimate_answer = 42

    await exit_event.wait()
    task.cancel()

if __name__ == &quot; __main__&quot;:
    asyncio.run(main(asyncio.Event()))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So now main() now receives an event, setup the ShutdownHandler, and then upon receiving from the event, cancel the background_task(). For completeness’ sake, we can also handle the CancelledError in background_task, as follows:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;async def background_task():
    try:
        while True:
            await asyncio.sleep(10)

            print(&quot;Print something&quot;)

    except asyncio.CancelledError:
        print(&quot;Bye bye&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;handling-exceptions&quot;&gt;&lt;strong&gt;Handling Exceptions&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;Before we conclude, is on error handling. When we work with asyncio enough, we will notice if an awaitable running in background fails, it is not always obvious to us. Fortunately, like signals, we can also attach a handler if an exception is raised. Back to our example project, let’s implement an ExceptionHandler for the purpose.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@dataclass
class ExceptionHandler:
    shutdown_handler: ShutdownHandler

    def __call__ (self, loop, context) -&amp;gt; None:
        logger.error(&quot;Application failed. context:%s&quot;, context)

        asyncio.create_task(self.shutdown_handler())
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then, add a new line to register the handler after the signal handler registration&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;async def init():
    ...

    loop.set_exception_handler(ExceptionHandler(ShutdownHandler())) 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;While this is a more a catch-all workaround, we should still properly handle exceptions in all our coroutines. For instance, in addition to CancelledError, we should probably also handle errors corresponding to network problems in dex. However we are keeping our example concise to avoid making this article into an epic novel.&lt;/p&gt;
&lt;h4 id=&quot;conclusion-mastering-asynchronous-task-orchestration&quot;&gt;Conclusion: Mastering Asynchronous Task Orchestration&lt;/h4&gt;
&lt;p&gt;That’s all I want to cover in this article. While we spent quite some time building the application, we still barely scratched the surface of the capability of AsyncIO. We discussed about how to start executing a coroutine, and then from there how to queue more tasks and awaitables into the event loop. We also discussed about exiting the program gracefully, and how to handle errors.&lt;/p&gt;
&lt;p&gt;I owe a lot to &lt;a rel=&quot;nofollow&quot; href=&quot;https://roguelynn.com/words/asyncio-we-did-it-wrong/&quot;&gt;this series of articles by Lynn Root&lt;/a&gt; while I was re-learning AsyncIO when rewriting &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/coolsilon/bigmeow_bot&quot;&gt;bigmeow&lt;/a&gt;, the chatbot project. Some part of the code in our example project today is adapted from her implementation. The complete project &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/Jeffrey04/asyncio-post&quot;&gt;can be found at GitHub&lt;/a&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/Jeffrey04/asyncio-post&quot;&gt;,&lt;/a&gt;,) with some slight changes.&lt;/p&gt;
&lt;p&gt;Thank you so much for reading this far, and I shall write again, next week.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;To ensure clarity and address potential language errors, this article received editorial assistance from a large language model. Please note that all ideas and concepts presented herein are my own. The code examples provided were not generated or edited by an LLM. If you’re interested in project collaboration or job opportunities, feel free to reach out to me via a message here on Medium or through LinkedIn for further discussion. This article was &lt;a rel=&quot;nofollow&quot; href=&quot;https://kitfucoda.medium.com/asyncio-task-management-a-hands-on-scheduler-project-bd7b7fe58c7e&quot;&gt;originally published at Medium&lt;/a&gt;.&lt;/p&gt;
</description>
<category>Adv Concepts</category>
<guid isPermaLink="true">https://coderlegion.com/1756/asyncio-task-management-a-hands-on-scheduler-project</guid>
<pubDate>Tue, 01 Apr 2025 06:46:58 +0000</pubDate>
</item>
<item>
<title>Understanding Awaitables: Coroutines, Tasks, and Futures in Python</title>
<link>https://coderlegion.com/1644/understanding-awaitables-coroutines-tasks-and-futures-in-python</link>
<description>&lt;p&gt;Previously, we explored &lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.to/jeffrey04/how-to-write-an-asyncio-telegram-bot-in-python-4hig&quot;&gt;chatbot building with AsyncIO&lt;/a&gt;. As I am committing to publish one article a week, I am constantly on the lookout for new ideas. Then I figured it may be a good opportunity to delve deeper into asynchronous programming. The writing plan eventually expanded to a series of articles, and we are starting with the concept of Awaitables today.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn-images-1.medium.com/max/1024/1*3uPI39CkDzqkT6OJUya19Q.jpeg&quot; alt=&quot;&quot;&gt;&lt;br&gt;
&lt;em&gt;An illustration from copilot on the topic&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;To a lot of us, our first exposure to something resembles asynchronous programming would be learning to write GUI applications. A graphical application usually responds to user events, that may happen any time while it is running. We then attach a handler function to events we are interested in, however we do not have much control when it is executed.&lt;/p&gt;
&lt;h4 id=&quot;the-javascript-prelude-from-callbacks-to-promises&quot;&gt;The JavaScript Prelude: From Callbacks to Promises&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn-images-1.medium.com/max/1024/0*RUsKxXI9tspcg5ee&quot; alt=&quot;&quot;&gt;&lt;br&gt;
&lt;em&gt;Photo by Womanizer Toys on Unsplash&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;I first learned programming when I was in college. The assignments were designed to train us to write code with a set of a controlled scope. I graduated in the era of web 2.0, that’s when people started to explore AJAX to add dynamic behaviour to web pages. Suddenly, despite we were plagued with inconsistencies among browser’s JavaScript implementation, webpages started to feel like a GUI application.&lt;/p&gt;
&lt;p&gt;AJAX, which is a short for Asynchronous JavaScript and XML, opens up the possibility for us to fetch data from a web server through JavaScript. With proper use, we can get a page to fetch additional information to update the page content, responding to various events defined in the document object model, DOM. Some examples of DOM events are, clicking on elements in page, elements loaded in page, or even timed events through setInterval and setTimeout.&lt;/p&gt;
&lt;p&gt;Despite having some experience with GUI application programming, but doing it in real enterprise setting feels very different. I was still too used to structured procedural programming, where code execution is often synchronous, in a mostly predictable order. However, when it comes to an enterprise setting, the requirements are constantly changing and time is often a luxury we cannot afford. Despite having good mentor, it still took me quite a while to have a basic grasp of asychronous programming with the delicate language construct at the time.&lt;/p&gt;
&lt;p&gt;Let's start with event handlers. Everything we see in a web page, is represented in a Document Object Model (DOM) tree, and each element is capable of responding to events. For instance, we can click on a button, and have it pop up an alert dialog through a code snippet below.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# In HTML markup
&amp;lt;button id=&quot;theButton&quot;&amp;gt;The button&amp;lt;/button&amp;gt;

# In JavaScript
const button = document.getElementById(&quot;theButton&quot;)
button.addEventListener((e) =&amp;gt; window.alert(&quot;Button is pressed&quot;))

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then we have callbacks, which were popularized by AJAX calls. Back then, with &lt;a rel=&quot;nofollow&quot; href=&quot;https://jquery.com/&quot;&gt;jQuery&lt;/a&gt;, we could define handlers to deal with both success or failure cases. For instance, let's say we want to fetch the HTML markup of this blog (skipping error failure callback for brevity), we do&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$.ajax({
    url: &quot;https://kitfucoda.medium.com/&quot;,
    success: (data, status, xhr) =&amp;gt; { /* process data here */ }
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It is worth noticing that both the event handler and the success callback we defined earlier, can only access variables that were defined before their definition. The handler and callback would have no access to new external variables. On the other hand, the code outside the callback and handler do not have access to variables within them. Essentially, we can treat the code being executed separately, completely breaking off from the normal execution flow. Therefore, we do not have a way to tell if they are executed, or when they are done (or failed).&lt;/p&gt;
&lt;p&gt;It is also possible to combine the two together. For example, combining the two examples above, we can make the page to fetch this blog after clicking on the button.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;button.addEventListener(
    (e) =&amp;gt; $.ajax({
        url: 'https://kitfucoda.medium.com',
        success: (data, status, xhr) =&amp;gt; { /* maybe populate this somewhere */ }
    })
)

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Remember we talked about how it is not possible to know if these handlers are done executing from the outside? Yes, nesting them was the only way to ensure order. If you need to fetch my other blog &lt;a rel=&quot;nofollow&quot; href=&quot;https://cslai.coolsilon.com&quot;&gt;https://cslai.coolsilon.com&lt;/a&gt; AFTER finishing fetching this blog, you can do another $.ajax call within the success handler above. The growing nested callbacks eventually led to what people call a callback hell or pyramid of doom.&lt;/p&gt;
&lt;p&gt;I didn’t follow the development of JavaScript much, as I moved on to doing Python. However we eventually had Promise, and my first encounter with it was again through jQuery. Now, it is possible to write a sequence of AJAX calls in a chain, instead of nesting endlessly. For instance&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$.ajax({url: 'https://kitfucoda.medium.com/'})
    .then((response) =&amp;gt; {
        // do something with the response
        // Now start a new ajax
        return $.ajax({url: 'https://cslai.coolsilon.com/'})
    })
    .then((response) =&amp;gt; {
        // Do other thing with the response
    })
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;No more pyramid of doom, but the callback and the code outside of it still remained separate and independent to each other.&lt;/p&gt;
&lt;h4 id=&quot;enter-asyncio-awaitables-defined&quot;&gt;Enter AsyncIO: Awaitables Defined&lt;/h4&gt;
&lt;p&gt;My exploration of asynchronous programming in Python, was just as chaotic. When I was working for a company that offers CDN service, I spent some time learning about &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.gevent.org/&quot;&gt;gevent and greenlets&lt;/a&gt;. It was practically my first introduction to the asynchronous world in the language. However, I left the company soon after that and didn’t have a chance to see it in action. Years later, I hacked a chatbot that barely worked, and also did a quick exercise &lt;a rel=&quot;nofollow&quot; href=&quot;https://cslai.coolsilon.com/2024/10/14/building-a-simple-asgi-websocket-application/&quot;&gt;building an ASGI websocket chatroom&lt;/a&gt;, without use of any frameworks. While I managed to do it, but it wasn’t a useful introduction to AsyncIO.&lt;/p&gt;
&lt;p&gt;This article, like the one I did on chatbot, is an article I wished I read back then.&lt;/p&gt;
&lt;h5 id=&quot;coroutines-the-foundation-of-awaitables&quot;&gt;Coroutines: The Foundation of Awaitables&lt;/h5&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn-images-1.medium.com/max/1024/0*cYxirWUqp9vR5x23&quot; alt=&quot;&quot;&gt;&lt;br&gt;
&lt;em&gt;Photo by Kelly Sikkema on Unsplash&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;As a prologue to the article I am writing next week, we cover &lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.python.org/3/library/asyncio-task.html#awaitables&quot;&gt;Awaitables&lt;/a&gt; this week in the context of AsyncIO. Understanding what they are is core to understanding how to program with AsyncIO. However, before we start, let’s talk about coroutines. A coroutine is a function defined with an async keyword. Note that if a function has any async or await statement, it has to be defined as a coroutine.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# a normal coroutine
async def a_coroutine():
    return 42

# this is invalid
def this_should_be_a_coroutine():
    await some_awaitable()
    return 42
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We often write functions doing heavy IO-bound work as coroutines, such that other CPU bound computation can still take place simultaneously. To execute these coroutines, we would need an event loop. Despite the name, I prefer seeing it as a timetable like structure, a place where it accepts Awaitables (like coroutines) to be scheduled to run.&lt;/p&gt;
&lt;p&gt;Let’s slowly build a fictional project, as a tool to explore more about asyncio in this series of articles. We start by defining a coroutine that simulates I/O operations, and use &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.python-httpx.org/&quot;&gt;httpx&lt;/a&gt; to make HTTP requests. Given a national Pokédex ID, we want to return the name of the corresponding Pokémon.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;async def dex(id: int) -&amp;gt; str:
    assert isinstance(id, int)

    async with httpx.AsyncClient() as client:
        response = await client.get(f&quot;https://pokeapi.co/api/v2/pokemon/{id}/&quot;)

        return f&quot;The pokemon with id {id} is {response.json()['name']}&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When a coroutine is called like how a function is called, it returns a Coroutine object. The returned object is still pending scheduling, hence the code isn’t executed at this point. If the coroutine does not get a chance to run, e.g. when the function calling the coroutine exits, dropping the reference to it.&lt;/p&gt;
&lt;p&gt;The quickest way to fix it is prefixing an await before it. However, like we discussed earlier, if a function contains an await, then it has to be defined as a coroutine with async. Therefore, going asynchronous usually means an all-in commitment. This also means porting code part-by-part to be asynchronous takes enormous effort.&lt;/p&gt;
&lt;p&gt;Using await is a way to schedule a coroutine to the event loop. The behaviour is similar to synchronous programming, it will attempt to start the execution as soon as possible, and will only execute the next line after a result is returned. But there are times we want to have more control, and that’s achieved by turning it to a task.&lt;/p&gt;
&lt;h5 id=&quot;tasks-scheduling-and-managing-coroutines&quot;&gt;Tasks: Scheduling and Managing Coroutines&lt;/h5&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn-images-1.medium.com/max/1024/0*2nJ2XtcsYAO7xII2&quot; alt=&quot;&quot;&gt;&lt;br&gt;
&lt;em&gt;Photo by airfocus on Unsplash&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;A task can be created, by passing in the coroutine object to asyncio.create_task. It returns a asyncio.Task object referencing the scheduled coroutine. Some control is offered through the returned Task object, for example we can check if it is done, cancel the execution, or set a callback when done.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; task = asyncio.create_task(dex(6)) 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn-images-1.medium.com/max/1024/0*LRSH7j5Jmv3qjXGF&quot; alt=&quot;&quot;&gt;&lt;br&gt;
&lt;em&gt;Photo by Halfcut Pokemon on Unsplash&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Unlike the await keyword, you can call asyncio.create_task even in a function, as long as an event loop is available and running. However, being an awaitable object, you can also block execution for subsequent statements with an await too, in a coroutine. However, it is typically used when you only want to revisit the state of execution later (or forget about it).&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;async def foo() -&amp;gt; str:
    task = asyncio.create_task(dex(104)

    # proceed with other work

    return await task 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn-images-1.medium.com/max/1024/0*mdB_kyfuyeVbxtAf&quot; alt=&quot;&quot;&gt;&lt;br&gt;
&lt;em&gt;Photo by Branden Skeli on Unsplash&lt;/em&gt;&lt;/p&gt;
&lt;h5 id=&quot;futures-representing-asynchronous-results&quot;&gt;Futures: Representing Asynchronous Results&lt;/h5&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn-images-1.medium.com/max/1024/0*wq076oix0uxpnLSy&quot; alt=&quot;&quot;&gt;&lt;br&gt;
&lt;em&gt;Photo by Zulfa Nazer on Unsplash&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Next, we have Futures, which is the parent class of the Task we just discussed. I personally find this tricky to explain, but while preparing for the article, I was informed that a future works the way a callback does (still remember?). Let’s say I have a very badly defined function to calculate nth Fibonacci number to simulate a CPU-bound operation&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def fibonacci(nth: int) -&amp;gt; int:
    assert nth &amp;gt; 0

    result = ()

    for i in count(1):
        match i:
            case 1:
                result = (0,)

            case 2:
                result += (1,)

            case _:
                result = result[1:] + (sum(result),)

    assert len(result) &amp;gt; 0

    return result[-1]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Asynchronous programming is useful in scenario with a lot of I/O operations. It ensures concurrency by making it possible for CPU-bound operations to work, while waiting for I/O. However, if a CPU-bound operation is taking forever, it would end up stalling everything else. Hence, it is advisible to throw the work to another thread or process. We can use loop.run_in_executor to create an asyncio.Future object to offload the work to another thread.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;future = (
    asyncio.get_running_loop()
        .run_in_executor(None, partial(fibonacci, 1500))
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The first argument can take either a ThreadPoolExecutor or a ProcessPoolExecutor, leave it as None to use the default ThreadPoolExecutor. The second argument takes a callable, we could have rewrite fibonacci with __call__ dunder &lt;a rel=&quot;nofollow&quot; href=&quot;https://kitfucoda.medium.com/the-versatility-of-call-a-python-developers-secret-weapon-a6bff776971a&quot;&gt;as discussed in the previous article&lt;/a&gt;, but using functools.partial is fine.&lt;/p&gt;
&lt;p&gt;Alternatively we can use asyncio.to_thread which is an helpful abstraction to avoid creating a Future ourselves. However, it returns a coroutine, so it has to be awaited, or scheduled as a Task via asyncio.create_task. Also, unlike loop.run_in_executor, we do not get to choose between a ThreadPoolExecutor or ProcessPoolExecutor.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;coro = asyncio.to_thread(fibonacci, 1000)

# Schedule it for execution
asyncio.create_task(coro)

# Or await directly
await coro
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;But we haven’t show how callbacks are possible with Futures. Let’s write a simpler example on this topic. Suppose we have a coroutine that has to be run concurrently in the background, streaming a file content over the internet, and we are interested in finding the line number of a specific line in it.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;async def scan_huge_file(future, need_this_line):
    for line_num, line in enumerate(some_huge_file()):
        if line is need_this_line:
            future.set_result(line_num)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then, when we schedule the tasks (again, we will dive deeper on this in the next article), we would have done something like this.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;loop = asyncio.get_running_loop()
future = loop.create_future()

asyncio.create_task(scan_huge_file(future, 'very critical line'))

# do other high-priority tasks
...

# when we are finally done with other tasks
number = await future 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The callback is done in scan_huge_file, through future.set_result. The function scheduling the coroutine, only checks if a line number is found, after it is done with other tasks. Of course we could have save a reference to the asyncio.create_task, and then make scan_huge_file return the line number so we can await the task, but this is just to show how a callback can be implemented.&lt;/p&gt;
&lt;p&gt;Now we know the three Awaitables, it is time to revisit what it is. As you may have guessed, it is just a term to describe these three types of objects, where we can use await to get the result.&lt;/p&gt;
&lt;h4 id=&quot;python-vs-javascript-comparison&quot;&gt;Python vs. JavaScript: Comparison&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn-images-1.medium.com/max/1024/0*YtGBGib8QDsfSgtG&quot; alt=&quot;&quot;&gt;&lt;br&gt;
&lt;em&gt;Photo by Oleksandr Chumak on Unsplash&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Asynchronous programming in JavaScript modelled the problem very differently, and hence we have a complete different design. We stopped at Promise just now, and we briefly discussed that because it is the root of the implementation.&lt;/p&gt;
&lt;p&gt;AsyncIO is doing a good job abstracting the event loop away from the users. For instance, we only started to call event loop methods, when we were discussing about Futures. The development experience is getting so much better that using await directly to the coroutine, or to the Task (which wraps a coroutine) should be sufficient for most cases.&lt;/p&gt;
&lt;p&gt;OTOH, event loop in JavaScript, at least in browser, is almost completely abstracted away from us.&lt;/p&gt;
&lt;p&gt;A friend came to me a few days ago, complaining how he is confused about asynchronous programming in the language. Why certain asynchronous functions were called with an await but some without. That prompted me to do a quick check on the topic, and I was quite surprised with the discovery. It turns out if I call an asynchronous function without using await, it will be scheduled in the background.&lt;/p&gt;
&lt;p&gt;The discussion session was the main inspiration to this article, sans the Python part. I started with how event handlers led to creation of callbacks, Promise, and ultimately joining the async-await family. Similar to Python, if a function contains any async or await statement in it, then it has to be defined as async.&lt;/p&gt;
&lt;p&gt;While on the surface both share similar syntax, but the underlying implementations are completely different. In Python asynchronous was designed around coroutines, while in JavaScript was through Promise. I hope by weaving examples from both language it helps to clarify doubts on the matter.&lt;/p&gt;
&lt;h4 id=&quot;looking-ahead-more-asyncio&quot;&gt;&lt;strong&gt;Looking Ahead: More AsyncIO&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn-images-1.medium.com/max/1024/0*b7OX8Kc-FR_Rm-cg&quot; alt=&quot;&quot;&gt;&lt;br&gt;
&lt;em&gt;Photo by Andrew Ly on Unsplash&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Originally I was about to write on schedule and manage tasks, alongside notes on error handling through a fictional project. However, after planning out the content, just Awaitables alone would be long enough for the week. Hence, we would start building a fictional project next week, and discuss the above mentioned topics. Feel free to subscribe to the email newsletter so you don’t miss it, and leave me messages if I have missed anything today.&lt;/p&gt;
&lt;p&gt;Thanks for reading, and I shall write again, next week.&lt;/p&gt;
&lt;p&gt;For transparency, I’d like to acknowledge that while the code and technical explanations presented in this article are entirely my own, I received editorial assistance from a large language model. This collaboration has helped refine the clarity and structure of my writing. If you’re interested in discussing project collaborations or job opportunities, please feel free to reach out to me &lt;a rel=&quot;nofollow&quot; href=&quot;https://kitfucoda.medium.com&quot;&gt;here on Medium&lt;/a&gt; or via &lt;a rel=&quot;nofollow&quot; href=&quot;https://linkedin.com/in/jeffrey04&quot;&gt;my LinkedIn profile&lt;/a&gt;. The article was &lt;a rel=&quot;nofollow&quot; href=&quot;https://kitfucoda.medium.com/understanding-awaitables-coroutines-tasks-and-futures-in-python-a9c12890d20d&quot;&gt;originally published to Medium&lt;/a&gt;.&lt;/p&gt;
</description>
<category>Adv Concepts</category>
<guid isPermaLink="true">https://coderlegion.com/1644/understanding-awaitables-coroutines-tasks-and-futures-in-python</guid>
<pubDate>Mon, 24 Mar 2025 18:58:34 +0000</pubDate>
</item>
</channel>
</rss>