<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Gregor Soutar]]></title><description><![CDATA[Welcome! This is where I document my, “Oh, that’s neat,” moments and perhaps the occasional deep dive into topics that interest me.]]></description><link>https://gregorsoutar.com</link><generator>RSS for Node</generator><lastBuildDate>Fri, 11 Sep 2026 13:46:20 GMT</lastBuildDate><atom:link href="https://gregorsoutar.com/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Adobe Photoshop Lightroom API]]></title><description><![CDATA[The Lightroom API enables partner applications to access and manage a subscriber's photo catalog. It allows developers to build applications that can access a user's photos, create albums, add photos, and retrieve user information. Unfortunately this...]]></description><link>https://gregorsoutar.com/adobe-photoshop-lightroom-api</link><guid isPermaLink="true">https://gregorsoutar.com/adobe-photoshop-lightroom-api</guid><category><![CDATA[lightroom]]></category><category><![CDATA[OAuth 2.0]]></category><category><![CDATA[APIs]]></category><category><![CDATA[Postman]]></category><dc:creator><![CDATA[Gregor Soutar]]></dc:creator><pubDate>Sat, 14 Jun 2025 23:21:28 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1749915442281/c28b3d01-ecc1-45f5-a828-7f82dbfc92f7.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>The <a target="_blank" href="https://developer.adobe.com/lightroom/lightroom-api-docs/">Lightroom API</a> enables partner applications to access and manage a subscriber's photo catalog. It allows developers to build applications that can access a user's photos, create albums, add photos, and retrieve user information. Unfortunately this API does not have any endpoints that allow for photos to be manipulated with Lightroom. This seems to only be possible through another paid subscription to <a target="_blank" href="https://developer.adobe.com/firefly-services/docs/lightroom/">Adobe Firefly</a>. Regardless, I was interested in exploring this API to see if I could refine the process that I take when importing photos into my library.</p>
<h2 id="heading-my-problem">My Problem</h2>
<p>At the moment I have a bit of a contrived setup for importing photos from my camera. I have a Raspberry Pi permanently setup to listen for the insertion of my camera’s memory card. The Pi automatically copies the images (and any videos) to a timestamped folder on my network attached storage (NAS) under a directory for the current year. I’ve found this to be a convenient and neat way to import and organise stuff. I later rename the timestamped folder to something more appropriate like “Father’s Day.”</p>
<p>Since getting a new camera, I have started using <a target="_blank" href="http://lightroom.adobe.com/">Adobe Lightroom</a>, and it has proven to be worth the investment. My workflow now involves manually creating a corresponding album in Lightroom, importing RAW photos from my NAS, editing them in Lightroom, and then saving both small and large JPEG versions back to my server as an archive.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1749926516351/5e2e2266-ce82-4762-937f-f52495178347.png" alt class="image--center mx-auto" /></p>
<p>Why not just store everything in Lightroom? Although it can store videos, the space would be used up very quickly, and upgrading to a higher storage tier would be expensive. My impression is that Lightroom storage is not designed for long-term archiving. It feels more like a scratch space, intended for editing your current work with just enough room to do so comfortably. Once finished, albums should be archived somewhere more permanent. For this reason, I find it necessary to bring the edited photos back onto my NAS. With the images on my NAS, I also gain the ability to conveniently view the photos and videos on our smart TV’s.</p>
<p>As I mentioned, the process can be a bit cumbersome. If I export images from Lightroom and later decide to make further edits, perhaps after learning something new or spotting ways to improve them, I have to export the images again and replace the versions stored on my NAS. Keeping everything in sync is a constant hassle.</p>
<p>The initial import from my NAS into Lightroom can also be quite slow. Each RAW photo taken by my camera is around 44MB, and transferring hundreds of them over WiFi can be a frustrating process.</p>
<h2 id="heading-how-the-lightroom-api-could-help-me">How the Lightroom API Could Help Me</h2>
<p>By using the Lightroom API, my hope would be that I could produce an application that would allow:</p>
<ol>
<li><p>My Raspberry Pi to create and fill albums in Lightroom directly when I insert my camera’s memory card.</p>
</li>
<li><p>Synchronisation of the small and large JPEG image versions with my server with changes detected by events.</p>
</li>
</ol>
<p>This would mean no waiting for images to import to Lightroom, and no worry over the local images being in sync with edits I make on Lightroom.</p>
<h2 id="heading-using-the-lightroom-api">Using the Lightroom API</h2>
<p>The first step in using the Lightroom API is to produce an integration to get an API key for the Lightroom API. To do this I followed the steps outlined <a target="_blank" href="https://developer.adobe.com/lightroom/lightroom-api-docs/getting-started/create_integration/">here</a>:</p>
<ol>
<li><p>Logged into the <a target="_blank" href="https://developer.adobe.com/console/">Adobe I/O Console</a> using my usual credentials.</p>
</li>
<li><p>Created a project.</p>
</li>
<li><p>Selected “Add API”</p>
</li>
<li><p>Added the “Lightroom Services API”</p>
</li>
<li><p>Selected “OAuth Single-Page App”</p>
</li>
</ol>
<p>With the API key (Client ID), I could then use <a target="_blank" href="https://www.postman.com/">Postman</a> to follow the OAuth authentication flow, generate access tokens, setup each endpoint using the <a target="_blank" href="https://developer.adobe.com/lightroom/lightroom-api-docs/api/">API reference</a>, and test if I could use them to:</p>
<ul>
<li><p>Upload an image ✅</p>
</li>
<li><p>Access a high-resolution version of an image ✅</p>
</li>
<li><p>Access a low-resolution version of an image ✅</p>
</li>
<li><p>Create an album ❌</p>
</li>
<li><p>Add images to album ✅</p>
</li>
</ul>
<p>I’ve made the workspace I was using public just in case it’s helpful to anyone trying to get to grips with the API. The workspace may be accessed using the button below.</p>
<p><a target="_blank" href="https://app.getpostman.com/run-collection/45882360-2faa4e41-d7d1-478b-804b-35335b505c71?action=collection%2Ffork&amp;source=rip_markdown&amp;collection-url=entityId%3D45882360-2faa4e41-d7d1-478b-804b-35335b505c71%26entityType%3Dcollection%26workspaceId%3D549e28a3-359c-4b4f-bd38-d7de219b57d0#?env%5BLightroom%20Env%5D=W3sia2V5IjoiQVBJX0tFWSIsInZhbHVlIjoiIiwiZW5hYmxlZCI6dHJ1ZSwidHlwZSI6InNlY3JldCIsInNlc3Npb25WYWx1ZSI6IiIsInNlc3Npb25JbmRleCI6MH0seyJrZXkiOiJTQ09QRSIsInZhbHVlIjoibHJfcGFydG5lcl9yZW5kaXRpb25fYXBpcywgQWRvYmVJRCwgb3BlbmlkLCBscl9wYXJ0bmVyX2FwaXMsIG9mZmxpbmVfYWNjZXNzIiwiZW5hYmxlZCI6dHJ1ZSwidHlwZSI6ImRlZmF1bHQiLCJzZXNzaW9uVmFsdWUiOiIiLCJzZXNzaW9uSW5kZXgiOjF9LHsia2V5IjoiQ0FUQUxPR19JRCIsInZhbHVlIjoiIiwiZW5hYmxlZCI6dHJ1ZSwidHlwZSI6ImRlZmF1bHQiLCJzZXNzaW9uVmFsdWUiOiIiLCJzZXNzaW9uSW5kZXgiOjJ9LHsia2V5IjoiQVNTRVRfSUQiLCJ2YWx1ZSI6IiIsImVuYWJsZWQiOnRydWUsInR5cGUiOiJkZWZhdWx0Iiwic2Vzc2lvblZhbHVlIjoiIiwic2Vzc2lvbkluZGV4IjozfSx7ImtleSI6IlVTRVJfSUQiLCJ2YWx1ZSI6IiIsImVuYWJsZWQiOnRydWUsInR5cGUiOiJkZWZhdWx0Iiwic2Vzc2lvblZhbHVlIjoiIiwic2Vzc2lvbkluZGV4Ijo0fSx7ImtleSI6IkFMQlVNX0lEIiwidmFsdWUiOiIiLCJlbmFibGVkIjp0cnVlLCJ0eXBlIjoiZGVmYXVsdCIsInNlc3Npb25WYWx1ZSI6IiIsInNlc3Npb25JbmRleCI6NX1d"><img src="https://run.pstmn.io/button.svg" alt="Run In Postman" class="image--center mx-auto" /></a></p>
<p>Unfortunately, these tests confirmed that it was not possible to create an album using the Lightroom API in the way that I had wanted. Adobe has implemented this feature in such a way that albums created using the Lightroom API will not show in the Lightroom application. This is confirmed by <a target="_blank" href="https://community.adobe.com/t5/lightroom-ecosystem-cloud-based-discussions/create-album-via-api/td-p/14567024">a community forum</a> post where the Adobe employee replies, “albums are specific to the partner who has created the album. It is only meant to seen on the partner and not on Lightroom applications.“ This is quite frustrating!</p>
<p>I think I would be able to tolerate having to manually create albums in Lightroom, it would still be a big win being able to upload images automatically. Next (and in another blog) I would like to explore events to confirm if it would be possible to trigger some sort of sync when a photo (asset) is edited. Only if this is possible would I pursue this project further.</p>
]]></content:encoded></item><item><title><![CDATA[Why use HashiCorp's Nomad?]]></title><description><![CDATA[I’ve recently started to work on a new (to me) project that will make use of the European Southern Observatory’s (ESO’s) new Extremely Large Telescope (ELT) instrument control software framework (IFW). I’m currently trying to understand various parts...]]></description><link>https://gregorsoutar.com/why-use-nomad</link><guid isPermaLink="true">https://gregorsoutar.com/why-use-nomad</guid><category><![CDATA[nomad]]></category><category><![CDATA[ESO ]]></category><category><![CDATA[ELT]]></category><dc:creator><![CDATA[Gregor Soutar]]></dc:creator><pubDate>Mon, 12 May 2025 18:58:06 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1746986573236/ad65c040-24c7-4976-918a-a12384eff422.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I’ve recently started to work on a new (to me) project that will make use of the European Southern Observatory’s (ESO’s) new <a target="_blank" href="https://elt.eso.org/">Extremely Large Telescope</a> (ELT) <a target="_blank" href="https://ftp.eso.org/pub/elt/repos/docs/ICS/public/">instrument control software framework</a> (IFW). I’m currently trying to understand various parts of this epic software infrastructure. A core part seems to be the use of <a target="_blank" href="https://developer.hashicorp.com/nomad">HashiCorp’s Nomad</a>. My aim here is to arrive at a basic understanding of what Nomad does and make sense of why Nomad was chosen.</p>
<h2 id="heading-what-is-nomad">What is Nomad?</h2>
<blockquote>
<p>Nomad is a flexible scheduler and workload orchestrator that enables you to deploy and manage any application across on-premise and cloud infrastructure at scale.</p>
</blockquote>
<p>That’s the official definition. To put it more simply, Nomad is like a logistics manager for software. As a developer, you define what your application needs and how it should run, and Nomad (the logistics expert) decides how and where to deploy it across your available infrastructure.</p>
<h2 id="heading-advertised-features">Advertised Features</h2>
<p><strong>Efficient resource usage</strong><br />Nomad makes smart use of your available infrastructure by packing tasks together on machines in a way that avoids waste. This approach, known as <em>bin packing</em>, helps ensure resources like CPU and memory are fully used without overloading any one system.</p>
<p><strong>Self-healing</strong><br />Nomad keeps an eye on all running tasks. If something crashes or stops responding, it automatically restarts or reschedules it elsewhere, helping to keep your services available and resilient.</p>
<p><strong>Zero downtime deployments</strong><br />Nomad supports safe deployment strategies that prevent user disruption. <em>Rolling updates</em> replace old versions with new ones gradually. <em>Blue/green deployments</em> run the new version alongside the old one until it is ready to fully take over. <em>Canary deployments</em> roll out changes to a small portion of users first, allowing issues to be caught early before affecting everyone.</p>
<p><strong>Supports many workload types</strong><br />Nomad is highly flexible, able to run a wide range of applications. This includes container workloads like Docker, Java applications in JAR files, virtual machines via QEMU, or even raw scripts and system commands.</p>
<p><strong>Cross-platform and portable</strong><br />Nomad runs as a single lightweight binary and works across Linux, Windows, and macOS. You can use it to manage workloads running in your data centre, in the cloud, or at the edge – all from one system.</p>
<p><strong>Simple, consistent job setup</strong><br />You describe each application using a clear job file, where you define what the app is, how it should run, where it should run, and how it connects to other services. This means whether you're running a container, a VM, or a script, the process for setting it up in Nomad is always the same.</p>
<h2 id="heading-key-terms">Key Terms</h2>
<p>The following image was taken from an <a target="_blank" href="https://developer.hashicorp.com/nomad/tutorials/get-started/gs-overview">introductory Nomad tutorial</a>, and I think gives a good overview of the core elements of the Nomad infrastructure.</p>
<p><img src="https://developer.hashicorp.com/_next/image?url=https%3A%2F%2Fcontent.hashicorp.com%2Fapi%2Fassets%3Fproduct%3Dtutorials%26version%3Dmain%26asset%3Dpublic%252Fimg%252Fnomad%252Fgs%252Fnomad_cluster_terms_diagram_dark.png%26width%3D960%26height%3D540%23dark-theme-only&amp;w=1920&amp;q=75&amp;dpl=dpl_8uXpqhZ3sSVRRxtrsu2cv3PvNmJV" alt="Diagram illustrating the Nomad cluster terms" /></p>
<h3 id="heading-setup-terms">Setup Terms</h3>
<p><strong>Agent</strong><br />An agent is a Nomad process that runs either as a server or a client. It forms the core of any Nomad deployment.</p>
<p><strong>Client</strong><br />A Nomad client runs the actual tasks. It registers itself with the servers and waits for work to be assigned. Clients are often called nodes, especially when discussing the infrastructure.</p>
<p><strong>Server</strong><br />Nomad servers handle all job scheduling and manage the clients. They decide where tasks should run and monitor the overall health of the system.</p>
<p><strong>Development agent</strong><br />A development agent is a special configuration used for local testing or learning. It runs as both a server and a client on the same machine and does not save any data to disk. This means it always starts in a clean, predictable state, making it ideal for quick experiments or demos.</p>
<h3 id="heading-operational-terms">Operational Terms</h3>
<p><strong>Task</strong><br />A task is the smallest unit of work in Nomad. It runs through a task driver such as Docker or Exec, which allows Nomad to support different types of workloads. Each task defines which driver it needs, along with its configuration, constraints, and required resources.</p>
<p><strong>Group</strong><br />A group is a collection of tasks that are run together on the same client. Tasks in a group share resources and are scheduled onto the same machine.</p>
<p><strong>Job</strong><br />A job is the main way to define and manage an application in Nomad. It includes everything Nomad needs to know to run one or more tasks, such as configurations, constraints, and deployment rules.</p>
<p><strong>Job specification</strong><br />Also called a ‘jobspec’, this is the complete definition of a job. It includes details such as the type of job, its tasks and resource needs, where it can run, and how it should behave. It’s written in a clear, structured format so Nomad can understand and act on it.</p>
<p><strong>Allocation</strong><br />An allocation is the result of Nomad placing a job on a specific client. It represents the link between a task group and the machine where it runs. When a job is launched, Nomad selects a suitable client and reserves resources on it for the job's tasks.</p>
<h2 id="heading-why-use-nomad">Why Use Nomad?</h2>
<p>Having worked on <a target="_blank" href="https://vltmoons.org/">MOONS</a> instrument control software, I am acutely aware of the number of (to use Nomad jargon) ‘tasks’ that will be required of the system, and that they may be distributed across several instrument or detector control workstations. Immediately, the use-case for Nomad becomes clear. As a method to define, deploy, manage, and oversee these hundreds of tasks, it seems like a wise choice. The idea that Nomad can automatically handle task distribution, restart failed processes, and ensure smooth deployments across multiple machines makes a lot of sense, especially for something as complex as the ELT system. Given all these capabilities, Nomad feels like it could play a crucial role in ensuring everything runs smoothly across these workstations on future Very Large Telescope (VLT) and ELT instruments.</p>
]]></content:encoded></item><item><title><![CDATA[Keeping a Service Running with systemd]]></title><description><![CDATA[Recently, I created a simple Python server to control a Sky Q box and an LG WebOS television programmatically. The goal was to build a straightforward interface for someone who finds modern technology overwhelming. The kind of person who would strugg...]]></description><link>https://gregorsoutar.com/keeping-a-service-running-with-systemd</link><guid isPermaLink="true">https://gregorsoutar.com/keeping-a-service-running-with-systemd</guid><category><![CDATA[systemd]]></category><category><![CDATA[Raspberry Pi]]></category><category><![CDATA[Python]]></category><dc:creator><![CDATA[Gregor Soutar]]></dc:creator><pubDate>Sat, 26 Apr 2025 16:22:14 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1745680334955/c53c723a-d2ec-4096-a6e0-b210bf45d549.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Recently, I created a simple Python server to control a Sky Q box and an LG WebOS television programmatically. The goal was to build a straightforward interface for someone who finds modern technology overwhelming. The kind of person who would struggle with navigating smart TV apps, juggling multiple remotes, or dealing with unreliable commercial software. I wanted to remove as many barriers as possible and make the experience as seamless as it could be.</p>
<p>One concern I had early on was reliability. The server was running precariously on a Raspberry Pi: if the Pi rebooted, the server wouldn't start automatically. If the server crashed for any reason, it would stay down until someone manually restarted it. For the person I was designing this for, it was absolutely essential that the system "just worked." If it became unreliable, there was a real risk they would lose faith, and with it their willingness to engage with the system at all.</p>
<p>Today, I implemented a proper solution using <strong>systemd</strong>, the standard service manager for Linux systems. Systemd allowed me to define my Python server as a service that automatically starts at boot, monitors itself, and restarts if it crashes unexpectedly. This small change will hopefully improve the overall reliability of the system.</p>
<h2 id="heading-how-to-set-it-up">How to set it up</h2>
<ol>
<li><p><strong>Create a</strong> <code>systemd</code> service file (example: <code>/etc/systemd/system/yourservicename.service</code>):</p>
<pre><code class="lang-ini"> <span class="hljs-section">[Unit]</span>
 <span class="hljs-attr">Description</span>=My Python Application  
 <span class="hljs-attr">After</span>=network.target <span class="hljs-comment"># Wait for network to load</span>

 <span class="hljs-section">[Service]</span>
 <span class="hljs-attr">ExecStart</span>=/path/to/venv/bin/python /path/to/your_app.py
 <span class="hljs-attr">WorkingDirectory</span>=/path/to/working/directory <span class="hljs-comment"># ensures relative file paths work correctly.</span>
 <span class="hljs-attr">Restart</span>=always <span class="hljs-comment"># Always restart if the server crashes</span>
 <span class="hljs-attr">RestartSec</span>=<span class="hljs-number">5</span>  <span class="hljs-comment"># Wait 5s before restarting the server </span>
 <span class="hljs-attr">User</span>=pi          

 <span class="hljs-section">[Install]</span>
 <span class="hljs-attr">WantedBy</span>=multi-user.target <span class="hljs-comment"># Start the service when we reach a fully booted system</span>
</code></pre>
</li>
<li><p><strong>Enable and start it:</strong></p>
<pre><code class="lang-bash"> sudo systemctl daemon-reload
 sudo systemctl <span class="hljs-built_in">enable</span> yourservicename.service
 sudo systemctl start yourservicename.service
</code></pre>
</li>
<li><p><strong>Check its status:</strong></p>
<pre><code class="lang-bash"> sudo systemctl status tv_control.service
</code></pre>
</li>
</ol>
<h2 id="heading-final-thoughts">Final thoughts</h2>
<p>By setting up the Python server as a systemd service, I've taken an important step toward ensuring that the system remains dependable and frictionless for the person using it. They shouldn’t have to think about whether the server is running or worry about fixing anything if something goes wrong, it should just work, quietly and reliably in the background. This approach not only improves technical stability but also builds trust, making it far more likely that the system will become a comfortable and lasting part of their daily routine.</p>
]]></content:encoded></item><item><title><![CDATA[Supporting a Digital Immigrant]]></title><description><![CDATA[I remember being called a “digital native” in school; someone who grew up with computers, the internet, and digital technology as a natural part of their environment. For us, the workflows and design languages that underpin digital technology are sec...]]></description><link>https://gregorsoutar.com/supporting-a-digital-immigrant</link><guid isPermaLink="true">https://gregorsoutar.com/supporting-a-digital-immigrant</guid><category><![CDATA[LGWebOs]]></category><category><![CDATA[Python]]></category><category><![CDATA[Accessibility]]></category><dc:creator><![CDATA[Gregor Soutar]]></dc:creator><pubDate>Tue, 15 Apr 2025 20:44:53 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1744723843824/e43dbe1a-6771-40cf-8b55-21c8f69300d4.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I remember being called a “digital native” in school; someone who grew up with computers, the internet, and digital technology as a natural part of their environment. For us, the workflows and design languages that underpin digital technology are second nature. We instinctively know to press and hold the power button to turn on a phone, to press the volume and power button to take a screenshot, to pinch or swipe at photos in our cameral roll, to check the top-right corner of a website for account options, or to head straight to the app store when we need to install a new tool or service. We can all fluently speak the language of digital technology.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1744570176818/6276bb65-f2c9-4842-b216-b0a4156660e3.png" alt="An illustration showing three concepts: taking a screenshot by pressing the volume and power buttons on a phone, using pinch to zoom in on a phone, and common website features including a logo, heading, text block, and account icon labeled &quot;Bob.&quot;" class="image--center mx-auto" /></p>
<p>Continuing with my teacher’s metaphor, people who came to digital technology later in life could be called digital immigrants. Unlike digital natives, they didn’t grow up with the design patterns and workflows that underpin modern devices and interfaces. They might not be fluent in our digital language of menus, windows, gestures and forms. It makes sense that tasks like navigating a smart TV menu, installing an app, or signing into a streaming service could feel foreign.</p>
<h1 id="heading-my-digital-immigrant">My Digital Immigrant</h1>
<p>There is one digital immigrant I spend time with often. Someone from the land of rotary phones, tube radios, and cutting-edge 1940s technology like black-and-white television. Unfortunately they have reached the “disheartened” or “uninterested” phase of digital engagement. To them, modern technology feels confusing and needlessly complex. Concepts that seem second nature to me, like the distinction between the TV remote and the set-top box (Sky television) remote, remain hard to internalise, even after repeated explanations. Opening an app on the television feels, to them, like trying to operate a super computer.</p>
<p>This individual has a favourite YouTube channel they enjoy watching, but they can only access it when someone is around to help. They need assistance to open the app, update it if necessary, search for the channel, open it, and choose a video they haven’t already seen. Just as I wouldn’t be able to follow instructions in Italian, they struggle with the unfamiliar language of digital technology. As a result, they often settle for whatever comes on when they turn on the TV - or nothing at all, especially if the set-top box doesn’t behave exactly as they expect.</p>
<p>With the television being this persons main source of entertainment, I wanted to try and think of a way to make their favourite YouTube channel more accessible to them.</p>
<h1 id="heading-the-plan">The Plan</h1>
<p>Find a way to, with the press of a single button:</p>
<ol>
<li><p>Turn on the TV</p>
</li>
<li><p>Open YouTube</p>
</li>
<li><p>Automatically Choose the “watch as guest” option - they don’t have or want an account.</p>
</li>
<li><p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1744741889980/4f845480-8d63-43bf-950b-40ade1dcf05c.png" alt="Illustration of the YouTube TV app loading screen. It shows a prompt asking &quot;Who's Watching?&quot; with two options: &quot;Add Account&quot; and &quot;YouTube Kids.&quot; The &quot;Add Account&quot; button is highlighted as selected by default. An instruction indicates the need to programmatically select the &quot;Watch As Guest&quot; button at the bottom." class="image--center mx-auto" /></p>
<p> Play a video from their favourite youtube channel that they haven’t seen before.</p>
</li>
</ol>
<h1 id="heading-controlling-the-tv-programatically">Controlling the TV Programatically</h1>
<h2 id="heading-the-research">The Research</h2>
<p>When I first started thinking about how to control the TV, I explored a few initial ideas, the first involving a microcontroller and IR blaster. The concept was simple: learn the remote codes and transmit a sequence of IR signals to turn the TV on, open YouTube, and perform other basic functions. But the more I considered it, the more fragile the solution seemed. For instance, if the TV was already on, sending an "on" command would actually turn it off. And if someone changed the order of apps on the home screen, the carefully timed sequence of button presses to launch YouTube would no longer work. The whole setup would rely on a brittle choreography of commands that could easily go out of sync. It would have been error prone and frustrating for the user.</p>
<p>Then I turned to the TV’s manual and was excited to find documentation about external control via a serial interface. This approach promised much more reliable functionality. You could send a definitive "power on" command, directly switch to a specific input source by name, or set an exact volume level; eliminating the guesswork and inconsistency of IR-based control. It was exactly the level of precision I was hoping for. Unfortunately, when I inspected the back of the TV, I realised there was no serial port available. I suppose this feature is only present on commercial models, and the manual was simply covering all possible variants. It was a bit of a letdown.</p>
<p>After more investigating I came across <a target="_blank" href="https://github.com/supersaiyanmode/PyWebOSTV">a Git Hub repository</a> that contained a Python library that would let me connect to the TV and do all sorts of things. Crucially it would let me:</p>
<ol>
<li><p>Connect to the television from a Python program (that will run on a Raspberry Pi)</p>
</li>
<li><p>Open YouTube by name, and even a specific YouTube video!</p>
</li>
<li><p>Programatically navigate and select the “watch as guest” option with the libraries facility to emulate basic up, down and ok button presses.</p>
</li>
</ol>
<p>In addition it would let me:</p>
<ol>
<li><p>Set the volume to a specific number.</p>
</li>
<li><p>Send notifications to the TV which I could use to tell the user what’s going on.</p>
</li>
</ol>
<p>The only thing this library couldn’t do was turn the TV on - a very important step. However, I realised (and the documentation for this library points out) that you can use Wake on LAN and send a ‘magic’ packet to the television to wake it - you just need to enable this feature on the TV.</p>
<p>With this library I am now able to accomplish steps 1 to 3 of ‘the plan’.</p>
<h2 id="heading-the-implementation">The Implementation</h2>
<h3 id="heading-environment-variables">Environment Variables</h3>
<p>There are three runtime parameters that are needed in this program.</p>
<pre><code class="lang-python">TV_ADDR=<span class="hljs-string">"XXX.XXX.XXX.XXX"</span> <span class="hljs-comment"># IP address of the television (or hostname if you're fancy)</span>
TV_MAC=<span class="hljs-string">"FF:FF:FF:FF:FF:FF"</span> <span class="hljs-comment"># Hardware address of the network interface on the TV</span>
WEBOS_KEY=<span class="hljs-string">"11c4bc438d420b33cc2334323d77dddf"</span> <span class="hljs-comment"># Access key returned by registering with TV</span>
</code></pre>
<p>I have chosen to store them in a .env file and use the <code>load_dotenv()</code> function to allow the values of the environment variables to be used by the program. This has to be done before any function that uses these parameters is used.</p>
<pre><code class="lang-python"><span class="hljs-keyword">from</span> dotenv <span class="hljs-keyword">import</span> load_dotenv

<span class="hljs-comment"># Load environment variables from .env file</span>
load_dotenv()
</code></pre>
<h3 id="heading-turning-on-the-tv">Turning on the TV</h3>
<p>The first component I needed was a function to turn the TV on using Wake on LAN. A quick Google revealed the <a target="_blank" href="https://pypi.org/project/wakeonlan/">wakeonlan Python package</a>, which was easily installed with pip. All I have to do is send the magic packet to the TV’s hardware address to get it to turn on. In testing I found that sending it twice was always reliable.</p>
<pre><code class="lang-python"><span class="hljs-keyword">from</span> wakeonlan <span class="hljs-keyword">import</span> send_magic_packet

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">turn_on_tv</span>():</span>
    tv_mac = os.getenv(<span class="hljs-string">'TV_MAC'</span>)

    <span class="hljs-comment"># Turn on the TV through wake on LAN</span>
    send_magic_packet(tv_mac)
    send_magic_packet(tv_mac)
</code></pre>
<h3 id="heading-determining-if-the-tv-is-on-the-network">Determining if the TV is on the Network</h3>
<p>I then wanted a component to indicate if the TV was online. If its already on, I don’t have to turn it on. I also don’t want to attempt to connect to the TV before the it is on the network. This function simply pings the television, and if it responds within a second we can be pretty confident its online. If not, then it probably isn’t.</p>
<pre><code class="lang-python"><span class="hljs-keyword">import</span> os 

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">tv_online</span>():</span>
    tv_addr = os.getenv(<span class="hljs-string">'TV_ADDR'</span>)

    <span class="hljs-comment"># Check if the TV is online by pinging it </span>
    <span class="hljs-comment"># -c 1 (send one packet)</span>
    <span class="hljs-comment"># -t 1 (wait at most one second for response)</span>
    response = os.system(<span class="hljs-string">f"ping -c 1 -t 1 <span class="hljs-subst">{tv_addr}</span>"</span>)
    <span class="hljs-keyword">if</span> response == <span class="hljs-number">0</span>:
        print(<span class="hljs-string">f"<span class="hljs-subst">{tv_addr}</span> is online"</span>)
        <span class="hljs-keyword">return</span> <span class="hljs-literal">True</span>
    <span class="hljs-keyword">else</span>:
        print(<span class="hljs-string">f"<span class="hljs-subst">{tv_addr}</span> is offline"</span>)
        <span class="hljs-keyword">return</span> <span class="hljs-literal">False</span>
</code></pre>
<h3 id="heading-connecting-to-the-television">Connecting to the Television</h3>
<p>The next piece of functionality was establishing a connection to the television. For this, I used code largely borrowed from the Python library I mentioned earlier. The first time you attempt to connect, the <code>tv_key</code> should be an empty dictionary. Without a client key, the TV will display a pop-up asking you to confirm the connection. Once you approve it on-screen, the registration completes and the program will print out a unique client key.</p>
<p>This key can then be added to a <code>.env</code> file and the <code>tv_key</code> line in the script can be uncommented. From that point forward, the program can connect automatically without requiring manual confirmation each time.</p>
<p>In my implementation, I added a loop that retries the connection every five seconds, up to a maximum number of attempts. I noticed that even when the TV was online and responding to pings, the WebOS service sometimes wasn’t ready to accept connections immediately. This retry loop gives the TV time to fully boot its network services. If it still can’t connect after several tries, the script gives up; useful in cases where something is genuinely wrong.</p>
<p>Ideally, I’d like to improve this in the future by making the registration process fully automatic, including storing the client key persistently. But for now, this solution works well for my needs.</p>
<pre><code class="lang-python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">connect_to_tv</span>(<span class="hljs-params">max_tries=<span class="hljs-number">10</span></span>):</span>
    tv_key = {}
    <span class="hljs-comment">#tv_key = {'client_key': os.getenv('WEBOS_KEY')}</span>
    tv_addr = os.getenv(<span class="hljs-string">'TV_ADDR'</span>)

    <span class="hljs-keyword">for</span> i <span class="hljs-keyword">in</span> range(max_tries):
        <span class="hljs-keyword">try</span>:
            <span class="hljs-comment"># Establish Connection With LG WebOS TV</span>
            client = WebOSClient(tv_addr)
            client.connect()
            <span class="hljs-keyword">for</span> status <span class="hljs-keyword">in</span> client.register(tv_key, timeout=<span class="hljs-number">10</span>):
                <span class="hljs-keyword">if</span> status == WebOSClient.PROMPTED:
                    print(<span class="hljs-string">"Please accept the connect on the TV!"</span>)
                <span class="hljs-keyword">elif</span> status == WebOSClient.REGISTERED:
                    print(<span class="hljs-string">"Registration successful!"</span>)
                    print(<span class="hljs-string">f"Store this as the environment variable WEBOS_KEY: <span class="hljs-subst">{tv_key}</span>"</span>)
                    <span class="hljs-keyword">return</span> client
        <span class="hljs-keyword">except</span> Exception <span class="hljs-keyword">as</span> e:
            print(<span class="hljs-string">f"Connection attempt <span class="hljs-subst">{i+<span class="hljs-number">1</span>}</span> failed: <span class="hljs-subst">{e}</span>"</span>)
            time.sleep(<span class="hljs-number">5</span>)
</code></pre>
<h3 id="heading-pulling-it-all-together">Pulling it all Together</h3>
<p>With all the key components in place, I built a main function to bring everything together and automate the experience from start to finish. Here's what it does:</p>
<ul>
<li><p>If the TV isn't already online, it powers it on and waits for it to become available on the network.</p>
</li>
<li><p>It establishes a connection using a previously registered key.</p>
</li>
<li><p>It sets the volume to a comfortable level (low enough to avoid surprises).</p>
</li>
<li><p>It notifies the user that everything is working and YouTube will launch shortly.</p>
</li>
<li><p>It fetches a new video from the user’s favourite channel (implementation covered in the next section).</p>
</li>
<li><p>It launches YouTube with the selected video.</p>
</li>
<li><p>It waits a bit to ensure the YouTube app has loaded - this part is admittedly guesswork, as there's no API feedback about app state.</p>
</li>
<li><p>Finally, it navigates the YouTube guest mode menu and starts playback.</p>
</li>
</ul>
<p>Here’s the function that brings it all together:</p>
<pre><code class="lang-python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">configure_tv_play_video</span>():</span> 
    <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> tv_online():
        turn_on_tv()

        <span class="hljs-comment"># Wait for the TV to appear on the network, try up to 10 times. </span>
        <span class="hljs-keyword">for</span> _ <span class="hljs-keyword">in</span> range(<span class="hljs-number">10</span>):
            <span class="hljs-keyword">if</span> tv_online():
                <span class="hljs-keyword">break</span>
            <span class="hljs-keyword">else</span>:
                time.sleep(<span class="hljs-number">3</span>)

    <span class="hljs-comment"># Establish Connection With LG WebOS TV</span>
    client = connect_to_tv()

    <span class="hljs-comment"># Set volume to something low </span>
    media = MediaControl(client)
    media.set_volume(<span class="hljs-number">8</span>)

    <span class="hljs-comment"># System control allows notifications to be sent to TV</span>
    system = SystemControl(client)
    system.notify(<span class="hljs-string">"Connected to the TV. Youtube will launch shortly."</span>)  

    <span class="hljs-comment"># Get a youtube video to watch </span>
    video = get_youtube_video()

    <span class="hljs-comment"># Launch YouTube on the TV with the selected video. </span>
    app = ApplicationControl(client)
    apps = app.list_apps()
    yt = [x <span class="hljs-keyword">for</span> x <span class="hljs-keyword">in</span> apps <span class="hljs-keyword">if</span> <span class="hljs-string">"youtube"</span> <span class="hljs-keyword">in</span> x[<span class="hljs-string">"title"</span>].lower()][<span class="hljs-number">0</span>]
    app.launch(yt, content_id=video)

    <span class="hljs-comment"># Wait for the app to launch</span>
    system.notify(<span class="hljs-string">"Waiting 15 seconds for Youtube to launch..."</span>)  
    time.sleep(<span class="hljs-number">15</span>)

    <span class="hljs-comment"># Select YouTube guest mode </span>
    inp = InputControl(client)
    inp.connect_input()
    inp.down()
    time.sleep(<span class="hljs-number">1</span>)
    inp.ok()
    inp.disconnect_input()

    system.notify(<span class="hljs-string">"Video should now be playing. Enjoy!"</span>)
</code></pre>
<h2 id="heading-getting-an-unwatched-youtube-video">Getting an Unwatched YouTube Video</h2>
<h3 id="heading-generic-youtube-channel-class">Generic YouTube Channel Class</h3>
<p>To retrieve a new video from the user’s favourite YouTube channel, I implemented a class that uses the <code>yt_dlp</code> library. This lets me scrape the necessary video metadata directly from a given YouTube channel.</p>
<p>The class, <code>YoutubeChannel</code>, is initialised with the name of the channel. When the <code>scrape_video_urls</code> method is called, it fetches all video entries from the channel's <code>/videos</code> page. For each entry, I check whether the video meets a defined acceptance criteria using <code>_accept_video</code>. By default the function always returns <code>True</code>, but I expect to overwrite this later when implementing a subclass for a specific channel.</p>
<p>Each video ID is added to a dictionary where the key is the ID and the value is a boolean indicating whether the video has been "watched" or not (we start by assuming all videos are unwatched). When a video is selected via <code>get_unwatched_video_url</code>, it randomly picks an unwatched one from the list, marks it as watched, and returns the ID so it can be launched in YouTube.</p>
<p>Here’s the class implementation:</p>
<pre><code class="lang-python"><span class="hljs-keyword">import</span> yt_dlp
<span class="hljs-keyword">import</span> random
<span class="hljs-keyword">from</span> urllib.parse <span class="hljs-keyword">import</span> urlparse, parse_qs

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">YoutubeChannel</span>:</span> 
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">__init__</span>(<span class="hljs-params">self, channel_name</span>):</span>
        self.channel_name = channel_name
        self.channel_url = <span class="hljs-string">f'https://www.youtube.com/@<span class="hljs-subst">{channel_name}</span>/videos'</span>
        self.video_urls = dict()

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">scrape_video_urls</span>(<span class="hljs-params">self</span>):</span>
        ydl_opts = {
            <span class="hljs-string">'quiet'</span>: <span class="hljs-literal">True</span>,
            <span class="hljs-string">'extract_flat'</span>: <span class="hljs-literal">True</span>,  <span class="hljs-comment"># Do not download, just get metadata</span>
            <span class="hljs-string">'skip_download'</span>: <span class="hljs-literal">True</span>,
        }

        <span class="hljs-keyword">with</span> yt_dlp.YoutubeDL(ydl_opts) <span class="hljs-keyword">as</span> ydl:
            info = ydl.extract_info(self.channel_url, download=<span class="hljs-literal">False</span>)
            entries = info.get(<span class="hljs-string">'entries'</span>, [])

            <span class="hljs-keyword">for</span> entry <span class="hljs-keyword">in</span> entries:
                <span class="hljs-keyword">if</span> self._accept_video(entry) <span class="hljs-keyword">and</span> entry[<span class="hljs-string">'url'</span>] <span class="hljs-keyword">not</span> <span class="hljs-keyword">in</span> self.video_urls:
                    parsed_url = urlparse(entry[<span class="hljs-string">'url'</span>])
                    query_params = parse_qs(parsed_url.query)
                    video_id = query_params.get(<span class="hljs-string">'v'</span>, [<span class="hljs-literal">None</span>])[<span class="hljs-number">0</span>]
                    self.video_urls[video_id] = <span class="hljs-literal">False</span> <span class="hljs-comment"># False means not watched yet</span>

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">get_video_urls</span>(<span class="hljs-params">self</span>):</span>
        <span class="hljs-keyword">return</span> list(self.video_urls.keys())

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">get_unwatched_video_urls</span>(<span class="hljs-params">self</span>):</span>
        unwatched = [url <span class="hljs-keyword">for</span> url, watched <span class="hljs-keyword">in</span> self.video_urls.items() <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> watched]

        <span class="hljs-comment"># If there are no unwatched videos, return all video URLs</span>
        <span class="hljs-keyword">if</span> len(unwatched) == <span class="hljs-number">0</span>:
            <span class="hljs-keyword">return</span> self.get_video_urls()

        <span class="hljs-keyword">return</span> unwatched

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">get_unwatched_video_url</span>(<span class="hljs-params">self</span>):</span>
        unwatched = self.get_unwatched_video_urls()
        chosen_video = random.choice(unwatched)
        self.video_urls[chosen_video] = <span class="hljs-literal">True</span>
        <span class="hljs-keyword">return</span> chosen_video

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">_accept_video</span>(<span class="hljs-params">self, entry</span>):</span>
         <span class="hljs-keyword">return</span> <span class="hljs-literal">True</span>
</code></pre>
<h3 id="heading-dedicated-youtube-channel-subclass">Dedicated YouTube Channel Subclass</h3>
<p>My family members favourite channel is <a target="_blank" href="https://www.youtube.com/@48hours">48 Hours</a>, so a dedicated subclass is implemented. I overwrite the <code>_accept_video</code> function so that only full episodes and no parts are loaded. As the videos are chosen at random, landing on Part 1 of a multi-part story, without Part 2 automatically following, and no easy way to find it manually, would be maddening. So, I’ve removed that possibility with this filter.</p>
<pre><code class="lang-python"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">YoutubeChannel48Hours</span>(<span class="hljs-params">YoutubeChannel</span>):</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">__init__</span>(<span class="hljs-params">self</span>):</span>
        super().__init__(<span class="hljs-string">'48hours'</span>)

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">_accept_video</span>(<span class="hljs-params">self, entry</span>):</span>
        <span class="hljs-keyword">return</span> <span class="hljs-string">"Full Episode"</span> <span class="hljs-keyword">in</span> entry[<span class="hljs-string">'title'</span>] <span class="hljs-keyword">and</span> <span class="hljs-string">"Part"</span> <span class="hljs-keyword">not</span> <span class="hljs-keyword">in</span> entry[<span class="hljs-string">'title'</span>]
</code></pre>
<h3 id="heading-persistent-storage-and-getting-videos">Persistent Storage and Getting Videos</h3>
<p>Keen readers may have noticed that, up to this point, there hasn’t been any persistent storage of the YouTube videos we've scraped or whether they’ve been watched. I handle this in the <code>get_youtube_video</code> function using Python’s built-in <code>pickle</code> library. This allows the entire object, complete with its internal state and watched/unwatched video list, to be serialised to disk and reloaded on future runs.</p>
<p>When the function is called, it first checks for the presence of a previously saved file. If the file exists, it loads the <code>YoutubeChannel48Hours</code> instance from disk, giving us instant access to the existing video list and watched statuses. If not, it creates a fresh instance of the class and populates it by scraping the channel again.</p>
<p>In either case, it then selects a random unwatched video. After that, the updated object, now with one more video marked as watched, is serialised and saved for next time. I thought this was a neat solution: no need to manually parse or manage separate data files, and it all just works with a couple of lines of code.</p>
<pre><code class="lang-python"><span class="hljs-keyword">import</span> pickle

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">get_youtube_video</span>():</span>
    <span class="hljs-comment"># Get a youtube video to watch </span>
    <span class="hljs-keyword">if</span> os.path.exists(<span class="hljs-string">'ytc_48_hours.pickle'</span>):
        <span class="hljs-keyword">with</span> open(<span class="hljs-string">'ytc_48_hours.pickle'</span>, <span class="hljs-string">'rb'</span>) <span class="hljs-keyword">as</span> f:
            ytc_48_hours = pickle.load(f)
    <span class="hljs-keyword">else</span>:
        ytc_48_hours = YoutubeChannel48Hours()
        ytc_48_hours.scrape_video_urls()

    video = ytc_48_hours.get_unwatched_video_url()

    <span class="hljs-comment"># Pickle youtube channel object(s) to file</span>
    <span class="hljs-keyword">with</span> open(<span class="hljs-string">'ytc_48_hours.pickle'</span>, <span class="hljs-string">'wb'</span>) <span class="hljs-keyword">as</span> f:
        pickle.dump(ytc_48_hours, f)

    <span class="hljs-keyword">return</span> video
</code></pre>
<h2 id="heading-controlling-from-a-button">Controlling from a Button</h2>
<p>The final step was making everything work with a single button press. I wanted this to be as simple as possible for my family member—no apps, no remotes, just a physical button.</p>
<p>From previous experiments, I had a 433 MHz wireless button and a compatible receiver lying around. Since everything else was already running on a Raspberry Pi, it made perfect sense to hook the receiver up to it and listen for the specific signal sent by this button. The Raspberry Pi has General-Purpose Input/Output (GPIO) pins that make this possible.</p>
<p>I wrote a small script using the <code>rpi-rf</code> library to monitor incoming RF codes. When it detects the code sent by this particular button, it calls the <code>configure_tv_play_video()</code> function and kicks off the whole process.</p>
<pre><code class="lang-python"><span class="hljs-keyword">import</span> time

<span class="hljs-keyword">from</span> rpi_rf <span class="hljs-keyword">import</span> RFDevice
<span class="hljs-keyword">from</span> webos <span class="hljs-keyword">import</span> configure_tv_play_video

RX_PIN = <span class="hljs-number">27</span>  
rfdevice = RFDevice(RX_PIN)
rfdevice.enable_rx()
timestamp = <span class="hljs-literal">None</span>

print(<span class="hljs-string">"Listening for RF signals..."</span>)

<span class="hljs-keyword">try</span>:
    <span class="hljs-keyword">while</span> <span class="hljs-literal">True</span>:
        <span class="hljs-keyword">if</span> rfdevice.rx_code_timestamp != timestamp:
            timestamp = rfdevice.rx_code_timestamp
            received_code = rfdevice.rx_code
            print(<span class="hljs-string">f"Received code: <span class="hljs-subst">{received_code}</span>"</span>)

            <span class="hljs-keyword">if</span> received_code == <span class="hljs-number">13739617</span>: <span class="hljs-comment"># the code sent from my RF button. </span>
                <span class="hljs-keyword">try</span>:
                    configure_tv_play_video()
                <span class="hljs-keyword">except</span> Exception <span class="hljs-keyword">as</span> e:
                    print(<span class="hljs-string">f"Couldn't configure tv and play video: <span class="hljs-subst">{e}</span>"</span>)

        time.sleep(<span class="hljs-number">0.01</span>)

<span class="hljs-keyword">except</span> KeyboardInterrupt:
    rfdevice.cleanup()
</code></pre>
<h2 id="heading-complete-code">Complete Code</h2>
<p>The complete code for my project can be found in this GitHub repository:</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://github.com/gs221/youtubeloader">https://github.com/gs221/youtubeloader</a></div>
<p> </p>
<h1 id="heading-leave-to-remain">Leave to Remain</h1>
<p>With this setup, my elderly family member, my digital immigrant, has finally crossed into the land of modern technology. With a single button press, the TV powers on (if it isn’t already), launches YouTube, and starts playing a new video from their favourite channel. If the TV is already on, it simply loads another video.</p>
<p>The one-button interface transforms what was once an intimidating, multi-step process into something effortless and accessible. It’s been amazing to see them confidently engage with a platform that used to feel completely out of reach. In a small but meaningful way, they now feel more connected to the digital world—and that, to me, makes the whole project worth it.</p>
]]></content:encoded></item><item><title><![CDATA[Python Data Classes]]></title><description><![CDATA[I recently took a brief look into Python Data Classes. From what I gathered, they're designed to create lightweight, boilerplate-free data containers. They're ideal for classes whose primary purpose is to store data and perform simple helper tasks li...]]></description><link>https://gregorsoutar.com/python-data-classes</link><guid isPermaLink="true">https://gregorsoutar.com/python-data-classes</guid><category><![CDATA[Python]]></category><category><![CDATA[dataclass]]></category><dc:creator><![CDATA[Gregor Soutar]]></dc:creator><pubDate>Sun, 06 Apr 2025 13:19:21 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1745692622772/152c6679-63e5-43af-a3a0-0181b2da0478.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I recently took a brief look into Python Data Classes. From what I gathered, they're designed to create lightweight, boilerplate-free data containers. They're ideal for classes whose primary purpose is to store data and perform simple helper tasks like formatting. However, they aren't well-suited for classes that handle complex logic, manage internal state, or interact with external systems.</p>
<p>Here is an example of a traditionally implemented Person class, and its equivalent data class implementation below. You can see that the data class implementation is much more concise.</p>
<pre><code class="lang-python"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Person</span>():</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">__init__</span>(<span class="hljs-params">self, forename, surname, age, sex</span>):</span>
        self.forename = forename
        self.surname = surname
        self.age = age
        self.sex = sex

<span class="hljs-meta">@dataclass</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Person</span>:</span>
    forename: str
    surname: str
    age: int
    sex: str
</code></pre>
<h2 id="heading-learning-about-python-data-classes">Learning About Python Data Classes</h2>
<p>I looked at two articles, and the official documentation:</p>
<ul>
<li><p><a target="_blank" href="https://docs.python.org/3/library/dataclasses.html">Python Documentation</a></p>
</li>
<li><p><a target="_blank" href="https://www.dataquest.io/blog/how-to-use-python-data-classes/">How to Use Python Data Classes in 2023 (A Beginner’s Guide)</a></p>
</li>
<li><p><a target="_blank" href="https://www.datacamp.com/tutorial/python-data-classes">Python Data Classes: A Comprehensive Tutorial</a></p>
</li>
</ul>
<p>I won't go into reproducing the examples here, but I recommend checking out these articles for some nice demonstrations. They provide valuable insights and practical examples that will give you a deeper understanding of how to use Python data classes effectively.</p>
<h2 id="heading-thoughts-on-python-data-classes">Thoughts on Python Data Classes</h2>
<p>After learning a bit about this Python feature, I had a think about some pros and cons.</p>
<h3 id="heading-pro-reduced-boilerplate">Pro: Reduced Boilerplate</h3>
<p>One of the main advantages of using data classes is the reduction of boilerplate code. Without data classes, you would need to manually write methods like:</p>
<ul>
<li><p><code>__init__()</code> that is used initialise attributes;</p>
</li>
<li><p><code>__repr__()</code> that is used to define the string representation for a class;</p>
</li>
<li><p><code>__eq__()</code> that is used for comparison.</p>
</li>
</ul>
<p>With a Python data class, these methods are automatically generated, giving developers a quick and robust set of core of features for the class. Additional features may be enabled, and any may be overwritten in the traditional way - by overriding the method implementation.</p>
<h3 id="heading-pro-improved-readability">Pro: Improved Readability</h3>
<p>I think that data classes enhance code readability by making the structure and purpose of the classes clearer. The class definition itself is compact, and the use of the <code>@dataclass</code> decorator signals that the class is meant for storing data, making its purpose clear. Furthermore, the generated <code>__repr__()</code> method provides a readable string representation of the object, which is immediately useful for debugging.</p>
<h3 id="heading-con-limited-utility">Con: Limited Utility</h3>
<p>While the automatic features of data classes, such as reducing boilerplate code and enhancing readability, are appealing, they do have limitations. Data classes are primarily designed to represent simple data structures. If you try to extend their use to more complex functionality or logic, they can quickly become cumbersome. Initially, a data class might seem like the perfect fit, but as the class evolves and requires more complex behaviour or internal state management, you may find yourself fighting against the design of the data class. In such cases, it could become more practical to refactor the class into a traditional class implementation, which may involve more effort but offers greater flexibility. While it’s tempting to use data classes for their convenience and automation, it's important to ensure that your use case aligns with their intended purpose. If the class is genuinely meant for simple data storage, data classes are a great choice, but if you're venturing into more complex logic, you might eventually find them a poor fit.</p>
]]></content:encoded></item><item><title><![CDATA[A Basic Local Retrieval-Augmented Generation (RAG) Tool with LangChain]]></title><description><![CDATA[LLMs Are Stuck in the Past
Unlike humans, who continuously seek, absorb, and apply new information, Large Language Models (LLMs) in their basic form are frozen in time. Their knowledge is limited to what they learned during their last pre-training. T...]]></description><link>https://gregorsoutar.com/a-basic-local-retrieval-augmented-generation-rag-tool-with-langchain</link><guid isPermaLink="true">https://gregorsoutar.com/a-basic-local-retrieval-augmented-generation-rag-tool-with-langchain</guid><category><![CDATA[llm]]></category><category><![CDATA[ollama]]></category><category><![CDATA[RAG ]]></category><category><![CDATA[langchain]]></category><category><![CDATA[Python]]></category><dc:creator><![CDATA[Gregor Soutar]]></dc:creator><pubDate>Sat, 22 Mar 2025 20:12:57 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1745694201149/d46adcd3-cff6-4ce9-bda5-9ca8c85d22fd.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-llms-are-stuck-in-the-past">LLMs Are Stuck in the Past</h2>
<p>Unlike humans, who continuously seek, absorb, and apply new information, Large Language Models (LLMs) in their basic form are frozen in time. Their knowledge is limited to what they learned during their last pre-training. They can’t naturally update themselves with new events or discoveries.</p>
<p>Pre-training is an expensive, resource-heavy process. It involves collecting vast amounts of text data and refining it into a statistical model that recognises patterns. Rather than storing knowledge like a database, the model learns relationships between words, allowing it to predict the next token in a sequence. While this creates the illusion of understanding, LLMs don’t actually "reason" or "remember" as humans do - they recognise patterns based on probability.</p>
<p>Since pre-training happens infrequently, LLMs have a knowledge cutoff. For example, if a model was last trained in November 2024, it won’t “know” anything beyond that date. So, if an LLM relies only on pre-training, it won’t be able to answer questions about recent news, ongoing events, or the latest scientific discoveries.</p>
<p>LLMs are strongest when answering questions about frequently discussed topics. If something is widely covered on the internet (like general science concepts, historical events, or common knowledge) it’s likely the model will provide accurate answers. But for rarer, less-documented information, its knowledge is hazy. For example, an LLM will confidently tell you the capital of Scotland or the laws of physics. But if you ask about a little-known research paper, a local news story, or an obscure hobby, it may struggle. It may even make something up.</p>
<h2 id="heading-a-familiar-fix">A Familiar Fix</h2>
<p>As humans, when we encounter something we don’t know, we turn to external resources like Google, instruction manuals, or documentation. For complex tasks like advanced math, we rely on specialised tools such as calculators. We recognise our own limitations and use these tools to help us reach the right answer.</p>
<p>LLMs can follow the same approach. By integrating real-time search capabilities, accessing external databases, or working alongside other specialised tools, they can overcome the gaps in their knowledge. Instead of relying solely on what they already know, they can be designed to seek out the right information, just like we do. One way of doing this is called Retrieval-Augmented Generation (RAG), which I have explored for the first time.</p>
<h2 id="heading-retrieval-augmented-generation-rag">Retrieval-Augmented Generation (RAG)</h2>
<p>In the simplest interactions with an LLM, you provide text as input, which is then tokenised and placed into the model's context window; the LLMs working memory. The LLM processes this input using its fixed model and generates a response solely based on learned patterns and probabilities. With RAG, your input (or prompt) is first processed to identify relevant external content from sources like databases, documents, or the internet. This retrieved information is then combined with your prompt and placed into the model's working memory. By incorporating this additional context, the LLM generates a more informed and accurate response, grounded in the relevant data.</p>
<p>I decided to explore the use-case where a set of documents serve as a knowledge base for a simple AI agent capable of answering questions about the documentation. My goal was to see if I could host an LLM locally on my laptop and have the agent function entirely offline, without relying on any paid models.</p>
<h2 id="heading-creating-the-rag-tool-with-langchain">Creating the RAG Tool with LangChain</h2>
<p>I did a bit of research, and watched a couple of YouTube tutorials around the topic.</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://www.youtube.com/watch?v=fFgyOucIFuk">https://www.youtube.com/watch?v=fFgyOucIFuk</a></div>
<p> </p>
<p>The first video provided a great overview of how to use your own documents effectively with an LLM. While a single document or a small set may sometimes be sufficient, handling larger information sets requires a more advanced approach like RAG.</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://www.youtube.com/watch?v=2TJxpyO3ei4">https://www.youtube.com/watch?v=2TJxpyO3ei4</a></div>
<p> </p>
<p>The next video introduced Ollama, a tool that allows you to run and manage large language models (LLMs) locally on your device, making it easier to experiment without relying on cloud services. It also covered LangChain and walked through a tutorial on building a simple RAG-based LLM agent using PDFs as a knowledge base. The process seemed straightforward and showed how easy it would be to host my own LLM instance on my laptop.</p>
<p>With these videos, and following the <a target="_blank" href="https://python.langchain.com/docs/tutorials/rag/">first part of a LangChain tutorial</a>, I was able to implement a simple local agent that is able to answer questions on my own documents.</p>
<h3 id="heading-choosing-a-model">Choosing a Model</h3>
<p>The first step in the build was to select a LLM and embeddings model. The LLM is the part that will take our query, context and produce a human-like response. The embedding model will be responsible for taking chunks of our documentation and producing embeddings.</p>
<p>Embeddings are a powerful way to represent words as numerical vectors in a high-dimensional space, capturing their semantic relationships. As a simple example, consider the words "king", "queen", "man", and "woman". An embedding model might assign them vectors:</p>
<p>$$\text{king} \rightarrow \begin{bmatrix} 0.8 \\ 0.6 \\ 0.7 \end{bmatrix}, \quad \text{queen} \rightarrow \begin{bmatrix} 0.8 \\ 0.7 \\ 0.6 \end{bmatrix}, \quad \text{man} \rightarrow \begin{bmatrix} 0.9 \\ 0.5 \\ 0.8 \end{bmatrix}, \quad \text{woman} \rightarrow \begin{bmatrix} 0.9 \\ 0.6 \\ 0.7 \end{bmatrix}$$</p><p>These numerical representations allow the model to capture meaningful relationships. Vector arithmetic can then be used to work with our text. For example, by performing the operation,</p>
<p>$$\text{king} - \text{man} + \text{woman}$$</p><p>$$\begin{bmatrix} 0.8 \\ 0.6 \\ 0.7 \end{bmatrix} - \begin{bmatrix} 0.9 \\ 0.5 \\ 0.8 \end{bmatrix} + \begin{bmatrix} 0.9 \\ 0.6 \\ 0.7 \end{bmatrix} = \begin{bmatrix} 0.8 \\ 0.7 \\ 0.6 \end{bmatrix}$$</p><p>the result is our vector, our embedding, of the word queen.</p>
<p>For this simple local agent I chose <code>llama3.2</code> as my LLM and <code>nomic-embed-text</code> as my embeddings model. Both were available through <a target="_blank" href="https://ollama.com/">Ollama</a>.</p>
<pre><code class="lang-python"><span class="hljs-comment"># Select chat model</span>
llm = OllamaLLM(model=<span class="hljs-string">"llama3.2"</span>)

<span class="hljs-comment"># Select embeddings model</span>
embeddings = OllamaEmbeddings(model=<span class="hljs-string">"nomic-embed-text"</span>)
</code></pre>
<h3 id="heading-selecting-a-vector-store">Selecting a Vector Store</h3>
<p>I then chose <a target="_blank" href="https://www.trychroma.com/">Chroma</a> as the vector store - a database that stores the embeddings. This is what was used in the second video. I didn’t like the idea of storing the embeddings in memory (suggested in the LangChain tutorial) where the embeddings would have to be re-created each time the program was run. The database provides persistent storage of the embeddings. Other options were available.</p>
<pre><code class="lang-python"><span class="hljs-comment"># Select vector store where embeddings are stored</span>
vector_store_dir = <span class="hljs-string">"chroma_db"</span>
vector_store = Chroma(embedding_function=embeddings, persist_directory=vector_store_dir)
</code></pre>
<h3 id="heading-loading-documents">Loading Documents</h3>
<p>Information may be loaded from many sources (websites, Word documents, PDFs) using the corresponding LangChain <a target="_blank" href="https://python.langchain.com/docs/concepts/document_loaders/">document loaders</a>. I was interested in loading all of the PDFs in a directory, so I use the <code>PyPDFDirectoryLoader</code>.</p>
<p>Loaders typically have <code>.load()</code> and <code>.lazy_load()</code> functions. The lazy load function returns a generator which I think can be helpful in applications that require memory and performance optimisation.</p>
<p>By default, the PDF document loader will not parse any images. I looked into enabling this feature with <a target="_blank" href="https://pypi.org/project/pytesseract/">Tesseract</a> as I liked the idea of the content of diagrams in my documentation being utilised. However, the specific PDFs I was interested in using had images that were unsupported by this optical character recognition (OCR) library. I proceeded without this potentially useful feature.</p>
<p>Once loaded, we get a list of documents. Each element in this list is a page from our PDFs. For example, if the folder contained a PDF with 2 pages, and another with 6, we would get a python list with 8 elements; our 8 pages.</p>
<pre><code class="lang-python"><span class="hljs-comment"># Meaningful name to describe the data being used in the vector store</span>
collection_name = <span class="hljs-string">"pdf_docs"</span>

<span class="hljs-comment"># Loader for all of the documents in the pdf_data directory</span>
pdf_loader = PyPDFDirectoryLoader(collection_name)
docs = pdf_loader.load()
</code></pre>
<h3 id="heading-splitting-documents">Splitting Documents</h3>
<p>Inserting entire pages of documents into a LLMs context window would be inefficient. It would be unlikely that an entire page would be relevant to your prompt, and models may struggle to find relevant content. As a next step we split our documents (our pages) into smaller chunks.</p>
<p>In this case the <code>RecursiveCharacterSplitter</code> is used to split pages into 1000 character chunks, with an overlap of 200 characters. These values wont be arbitrary, but I have not experimented with larger or smaller chunks sizes, and simply took the values from the LangChain tutorial. It would be interesting to see the effect that different chunk sizes and overlap had.</p>
<p>As a concrete example, I gave my agent two PDFs totalling 231 pages, so 231 documents. This was split into 681 smaller chunks. Note that the chunks have useful meta data like the name of the original document they came from.</p>
<pre><code class="lang-python"><span class="hljs-comment"># Split the documents</span>
text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=<span class="hljs-number">1000</span>,       <span class="hljs-comment"># chunk size (characters)</span>
    chunk_overlap=<span class="hljs-number">200</span>,     <span class="hljs-comment"># chunk overlap (characters)</span>
    add_start_index=<span class="hljs-literal">True</span>,  <span class="hljs-comment"># track index in original document</span>
)
all_splits = text_splitter.split_documents(docs)
</code></pre>
<h3 id="heading-adding-new-splits-to-the-database">Adding New Splits to the Database</h3>
<p>I was keen for the embeddings not to be re-generated with each run of my agent. If I was using a paid-for model to generate embeddings, I would be concerned with the needless expense of doing this repeatedly. For free models, I am more concerned with the time it takes to generate the embeddings constantly, and the time this takes.</p>
<p>I was able to follow documentation on <a target="_blank" href="https://python.langchain.com/docs/how_to/indexing">LangChain Indexing</a> that outlined how to efficiently manage embeddings. I provide the indexing function with document chunks, a <code>record_manager</code> that allows inspection of the database contents, and a Chroma <code>vector_store</code> configured with our chosen embedding model. I opt for the <code>'full'</code> cleanup mode, which ensures efficient management by:</p>
<ul>
<li><p>De-duplicating content</p>
</li>
<li><p>Removing chunks whose source PDFs no longer exist</p>
</li>
<li><p>Updating chunks when their original PDFs have changed</p>
</li>
<li><p>Retaining chunks that remain unchanged in both the database and the PDF folder</p>
</li>
</ul>
<p>The <code>source_id_key</code> is set to <code>'source'</code>, ensuring that each chunk is associated with its original PDF filename for tracking and management.</p>
<pre><code class="lang-python"><span class="hljs-comment"># Used to manage the contents of the Chroma vector store. </span>
record_manager = SQLRecordManager(namespace, db_url=<span class="hljs-string">f"sqlite:///<span class="hljs-subst">{vector_store_dir}</span>/chroma.db"</span>)
record_manager.create_schema()

<span class="hljs-comment"># Add new or updated chunk embeddings to the database </span>
index_result = index(all_splits, record_manager, vector_store, cleanup=<span class="hljs-string">'full'</span>, source_id_key=<span class="hljs-string">'source'</span>)
</code></pre>
<h3 id="heading-creating-the-prompt">Creating the Prompt</h3>
<p>I created an prompt that will be used whenever the agent is asked a question. Here we ‘configure’ the LLM to be an agent that answers a question based on the content. The agent should answer honestly if the answer cannot be derived from the content. The user’s question, and the retrieved context, are substituted at runtime into the <code>{question}</code> and <code>{context}</code> parts of the prompt.</p>
<pre><code class="lang-python">prompt = PromptTemplate.from_template(<span class="hljs-string">"You are an assistant for question-answering tasks. \
                                       Use the following pieces of retrieved context to answer the question. \
                                       If you don't know the answer, say that you don't know.\
                                       \nQuestion: {question} \
                                       \nContext: {context} \nAnswer:"</span>)
</code></pre>
<h3 id="heading-managing-state">Managing State</h3>
<p>From what I understand, LangGraph applications manage state through a typed dictionary. The intention is for the input, intermediate and output data to be kept track of with this. For our application, the question (input data), context (intermediate data transferred between steps), and answer (output data) are managed with this structure.</p>
<pre><code class="lang-python"><span class="hljs-comment"># Keep track of the state of the input question, retireved context and genrated answer </span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">State</span>(<span class="hljs-params">TypedDict</span>):</span>
    question: str
    context: List[Document]
    answer: str
</code></pre>
<h3 id="heading-application-steps">Application Steps</h3>
<p>Next we define the steps to be taken, LangChain refers to these as nodes. In our case we have a retrieval and generate node. Both steps are provided with the state of our application, and so have access to the question, context and answer.</p>
<p>The retrieve step takes the users question and performs a similarity search of our vector store. The result of the search is the four most relevant chunks of our documents. Four just happens to be the default value for the number of chunks to return and can be increased at will. I have not experimented with this.</p>
<p>This output of this node forms the context for our user’s question.</p>
<pre><code class="lang-python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">retrieve</span>(<span class="hljs-params">state: State</span>):</span>
    <span class="hljs-comment"># Search the vector store for the most relevant documents to the input question</span>
    retrieved_docs = vector_store.similarity_search(state[<span class="hljs-string">"question"</span>])
    <span class="hljs-comment"># This forms the context for our search</span>
    <span class="hljs-keyword">return</span> {<span class="hljs-string">"context"</span>: retrieved_docs}
</code></pre>
<p>The generate step fills our prompt outline with the users question and the context that was retrieved. This is then passed as the prompt to the LLM which provides a response to the user’s question.</p>
<pre><code class="lang-python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">generate</span>(<span class="hljs-params">state: State</span>):</span>
    <span class="hljs-comment"># Join all the documents in the context into a single string</span>
    docs_content = <span class="hljs-string">"\n\n"</span>.join(doc.page_content <span class="hljs-keyword">for</span> doc <span class="hljs-keyword">in</span> state[<span class="hljs-string">"context"</span>])
    <span class="hljs-comment"># Generate the answer to the input question</span>
    messages = prompt.invoke({<span class="hljs-string">"question"</span>: state[<span class="hljs-string">"question"</span>], <span class="hljs-string">"context"</span>: docs_content})
    <span class="hljs-comment"># Ask LLM to answer the question</span>
    response = llm.invoke(messages)
    <span class="hljs-comment"># LLM response forms our answer</span>
    <span class="hljs-keyword">return</span> {<span class="hljs-string">"answer"</span>: response}
</code></pre>
<h3 id="heading-connecting-the-steps">Connecting the Steps</h3>
<p>These individual steps (or nodes) are then compiled into a graph forming a sequence of steps. Here a <a target="_blank" href="https://langchain-ai.github.io/langgraph/reference/graphs/#langgraph.graph.state.StateGraph">StateGraph</a>, “a graph whose nodes communicate by reading and writing to a shared state,” is used. Our application starts with the retrieval step and then performs the generation step.</p>
<pre><code class="lang-python"><span class="hljs-comment"># Compile application into a graph object that connects the retrieval and generation steps into single sequence. </span>
graph_builder = StateGraph(State).add_sequence([retrieve, generate])
graph_builder.add_edge(START, <span class="hljs-string">"retrieve"</span>)
graph = graph_builder.compile()
</code></pre>
<h3 id="heading-asking-the-question">Asking the Question</h3>
<p>We can then use our graph to form a very basic application that repeatedly asks for the user to ask a question relating to the documents provided. LangGraphs support multiple invocation methods, in this case I have chosen to use the ‘stream’ method that lets me get the output of each step as the graph is executed.</p>
<pre><code class="lang-python"><span class="hljs-comment"># Ask user to enter a question</span>
question = input(<span class="hljs-string">"Please enter a question relating to the documents you have provided:"</span>)

<span class="hljs-keyword">while</span> question:
    <span class="hljs-comment"># Crudely print the context and the answer to the question.</span>
    <span class="hljs-keyword">for</span> step <span class="hljs-keyword">in</span> graph.stream({<span class="hljs-string">"question"</span>: question}, stream_mode=<span class="hljs-string">"updates"</span>):
        print(<span class="hljs-string">f"<span class="hljs-subst">{step}</span>\n\n----------------\n"</span>)

    <span class="hljs-comment"># Ask user to enter another question</span>
    question = input(<span class="hljs-string">"Please enter a question relating to the documents you have provided:"</span>)
</code></pre>
<h2 id="heading-example-output">Example Output</h2>
<p>As a test I created a knowledge-base of two PDF’s containing the instructions to two fictional board games; generated by ChatGPT. You can see, from the output below, the question that I asked, the 4 most relevant chunks of context that were retrieved (alongside their metadata), and the useful (correct) answer that the application was able to provide based on the context.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1742670392073/9803ae2d-02dc-4d15-9272-abdb316ba17d.png" alt="Fictional instructions from the Realm of the Arcane Lords board game." class="image--center mx-auto" /></p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"question"</span>: <span class="hljs-string">"In realm of the arcane lords, what determines the order of play?"</span>,
  <span class="hljs-attr">"context"</span>: [
    {
      <span class="hljs-attr">"content"</span>: <span class="hljs-string">"Realm of the Arcane Lords: A Game of Magic, Strategy, and Diplomacy Objective of the Game: The ultimate goal of \"Realm of the Arcane Lords\" is to ascend to the title of Supreme Arcane Lord by accumulating Arcane Power, forming strategic alliances, and tactically outmaneuvering your opponents across the mystical lands of Eldorin. A player wins by either amassing 100 Arcane Points (AP) or by securing dominance over three of the four Ancient Elemental Obelisks scattered across the game board. Components: \u2022 1 Large Game Board depicting the world of Eldorin, divided into 48 Territories \u2022 5 Sets of 15 Territory Control Markers per player \u2022 200 Arcane Power Tokens (AP) \u2022 4 Elemental Obelisk Tokens (Earth, Fire, Water, Air) \u2022 120 Spell Cards (divided into Attack, Defense, Support, and Forbidden Magic categories) \u2022 1 20-sided die (D20) and 3 6-sided dice (D6) \u2022 100 Resource Cards (Gold, Crystals, Herbs, and Relics) \u2022 6 Player Faction Boards representing different mystical orders \u2022 50 Miniature"</span>,
      <span class="hljs-attr">"metadata"</span>: {
        <span class="hljs-attr">"creationdate"</span>: <span class="hljs-string">"D:20250322185747Z00'00'"</span>,
        <span class="hljs-attr">"creator"</span>: <span class="hljs-string">"PyPDF"</span>,
        <span class="hljs-attr">"moddate"</span>: <span class="hljs-string">"D:20250322185747Z00'00'"</span>,
        <span class="hljs-attr">"page"</span>: <span class="hljs-number">0</span>,
        <span class="hljs-attr">"page_label"</span>: <span class="hljs-string">"1"</span>,
        <span class="hljs-attr">"producer"</span>: <span class="hljs-string">"macOS Version 14.6.1 (Build 23G93) Quartz PDFContext"</span>,
        <span class="hljs-attr">"source"</span>: <span class="hljs-string">"pdf_docs/Realm of the Arcane Lords.pdf"</span>,
        <span class="hljs-attr">"start_index"</span>: <span class="hljs-number">0</span>,
        <span class="hljs-attr">"total_pages"</span>: <span class="hljs-number">2</span>
      }
    },
    {
      <span class="hljs-attr">"content"</span>: <span class="hljs-string">"territories. \u2022 Forbidden Magic Spells can only be used once per game and carry significant risk (e.g., backfiring catastrophically). \u2022 Players may attempt to Bargain with the Elders\u2014a mysterious game mechanic where a player rolls three D6 dice and consults the Elder\u2019s Fate Table to receive a boon or curse. Final Thoughts: \"Realm of the Arcane Lords\" is a game that rewards strategy, diplomacy, and careful planning. Players must weigh the risks of confrontation against the benefits of alliance and resource gathering. Every choice carries weight, and only the most cunning and powerful will rise to claim the title of Supreme Arcane Lord!"</span>,
      <span class="hljs-attr">"metadata"</span>: {
        <span class="hljs-attr">"creationdate"</span>: <span class="hljs-string">"D:20250322185747Z00'00'"</span>,
        <span class="hljs-attr">"creator"</span>: <span class="hljs-string">"PyPDF"</span>,
        <span class="hljs-attr">"moddate"</span>: <span class="hljs-string">"D:20250322185747Z00'00'"</span>,
        <span class="hljs-attr">"page"</span>: <span class="hljs-number">1</span>,
        <span class="hljs-attr">"page_label"</span>: <span class="hljs-string">"2"</span>,
        <span class="hljs-attr">"producer"</span>: <span class="hljs-string">"macOS Version 14.6.1 (Build 23G93) Quartz PDFContext"</span>,
        <span class="hljs-attr">"source"</span>: <span class="hljs-string">"pdf_docs/Realm of the Arcane Lords.pdf"</span>,
        <span class="hljs-attr">"start_index"</span>: <span class="hljs-number">794</span>,
        <span class="hljs-attr">"total_pages"</span>: <span class="hljs-number">2</span>
      }
    },
    {
      <span class="hljs-attr">"content"</span>: <span class="hljs-string">"8. Players take turns placing one Miniature Wizard onto an unoccupied territory until all starting positions are chosen. Gameplay Mechanics: Each turn consists of four phases: 1. The Arcane Planning Phase: o Players may exchange resources, form temporary alliances, or trade Spell Cards. o Players may activate passive abilities from their faction board. o Players may draft one new Spell Card from the deck. 2. The Tactical Movement Phase: o Players may move their wizards across the board, with a maximum movement of two spaces per turn. o Entering an opponent-controlled territory initiates a Duel of Arcane Might (combat sequence). 3. The Duel of Arcane Might: o The attacker rolls a D20 and adds any applicable bonuses from Spell Cards, resources, or faction abilities."</span>,
      <span class="hljs-attr">"metadata"</span>: {
        <span class="hljs-attr">"creationdate"</span>: <span class="hljs-string">"D:20250322185747Z00'00'"</span>,
        <span class="hljs-attr">"creator"</span>: <span class="hljs-string">"PyPDF"</span>,
        <span class="hljs-attr">"moddate"</span>: <span class="hljs-string">"D:20250322185747Z00'00'"</span>,
        <span class="hljs-attr">"page"</span>: <span class="hljs-number">0</span>,
        <span class="hljs-attr">"page_label"</span>: <span class="hljs-string">"1"</span>,
        <span class="hljs-attr">"producer"</span>: <span class="hljs-string">"macOS Version 14.6.1 (Build 23G93) Quartz PDFContext"</span>,
        <span class="hljs-attr">"source"</span>: <span class="hljs-string">"pdf_docs/Realm of the Arcane Lords.pdf"</span>,
        <span class="hljs-attr">"start_index"</span>: <span class="hljs-number">1602</span>,
        <span class="hljs-attr">"total_pages"</span>: <span class="hljs-number">2</span>
      }
    },
    {
      <span class="hljs-attr">"content"</span>: <span class="hljs-string">"categories) \u2022 1 20-sided die (D20) and 3 6-sided dice (D6) \u2022 100 Resource Cards (Gold, Crystals, Herbs, and Relics) \u2022 6 Player Faction Boards representing different mystical orders \u2022 50 Miniature Wizards representing each player\u2019s controlled sorcerers \u2022 1 Rulebook with extended lore Setup: 1. Each player selects a mystical order and receives the corresponding Faction Board. 2. Players each take 15 Territory Control Markers in their faction\u2019s color. 3. Shuffle and place the Spell Cards in their respective piles. 4. Shuffle the Resource Deck and place it near the board. 5. Distribute 10 Arcane Power Tokens to each player. 6. Randomly place the four Elemental Obelisk Tokens in different quadrants of the board. 7. Players roll the D20 to determine the order of play; highest roll goes first. 8. Players take turns placing one Miniature Wizard onto an unoccupied territory until all starting positions are chosen. Gameplay Mechanics: Each turn consists of four phases: 1. The Arcane Planning"</span>,
      <span class="hljs-attr">"metadata"</span>: {
        <span class="hljs-attr">"creationdate"</span>: <span class="hljs-string">"D:20250322185747Z00'00'"</span>,
        <span class="hljs-attr">"creator"</span>: <span class="hljs-string">"PyPDF"</span>,
        <span class="hljs-attr">"moddate"</span>: <span class="hljs-string">"D:20250322185747Z00'00'"</span>,
        <span class="hljs-attr">"page"</span>: <span class="hljs-number">0</span>,
        <span class="hljs-attr">"page_label"</span>: <span class="hljs-string">"1"</span>,
        <span class="hljs-attr">"producer"</span>: <span class="hljs-string">"macOS Version 14.6.1 (Build 23G93) Quartz PDFContext"</span>,
        <span class="hljs-attr">"source"</span>: <span class="hljs-string">"pdf_docs/Realm of the Arcane Lords.pdf"</span>,
        <span class="hljs-attr">"start_index"</span>: <span class="hljs-number">804</span>,
        <span class="hljs-attr">"total_pages"</span>: <span class="hljs-number">2</span>
      }
    }
  ],
  <span class="hljs-attr">"answer"</span>: <span class="hljs-string">"According to the context, players roll a D20 to determine the order of play; the highest roll goes first."</span>
}
</code></pre>
<p>Asking the LLM the same question without any context of our made up game, of course, results in a made-up answer.</p>
<p><em>“In realm of the arcane lords, what determines the order of play? In the board game "Realm of the Arcane Lords", the order of play is determined by a unique mechanism called the "Arcane Order" track. Each player has three Arcane Points (AP) that they can use to determine their turn. The AP are distributed at the beginning of the game, and players take turns playing cards from their deck in a specific sequence based on the number of AP they have available.“</em></p>
<h2 id="heading-rag-recap">RAG Recap</h2>
<p>I've really enjoyed learning about RAG, and I see it as a powerful tool that complements. For me, its biggest potential lies in navigating complex proprietary software documentation, things like company policies, dense technical manuals, and inaccessible stacks of information. With RAG, all of this could become easily searchable through a simple Q&amp;A session in a more mature application.</p>
]]></content:encoded></item><item><title><![CDATA[My Takeaways from Andrej Karpathy's "How I Use LLMs"]]></title><description><![CDATA[Today I watched a really interesting video by Andrej Karpathy, a computer scientist and AI researcher who was a founding member at OpenAI. The video went into a nice level of detail in the ways that Andrej makes use of Large Language Models (LLMs). T...]]></description><link>https://gregorsoutar.com/my-takeaways-from-andrej-karpathys-how-i-use-llms</link><guid isPermaLink="true">https://gregorsoutar.com/my-takeaways-from-andrej-karpathys-how-i-use-llms</guid><category><![CDATA[llm]]></category><dc:creator><![CDATA[Gregor Soutar]]></dc:creator><pubDate>Sun, 09 Mar 2025 11:01:06 GMT</pubDate><content:encoded><![CDATA[<p>Today I watched a really interesting video by Andrej Karpathy, a computer scientist and AI researcher who was a founding member at OpenAI. The video went into a nice level of detail in the ways that Andrej makes use of Large Language Models (LLMs). The video gave me some insight into how LLMs work, but it mainly inspired me to think about how I can use LLMs more effectively while being aware of their limitations.</p>
<h2 id="heading-the-video">The Video</h2>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://www.youtube.com/watch?v=EWvNQjAaOHw">https://www.youtube.com/watch?v=EWvNQjAaOHw</a></div>
<p> </p>
<h2 id="heading-my-takeaways">My Takeaways</h2>
<p>I’ve tried to summarise the main things that I have taken from the video. This is my first time learning about a lot of the concepts here, so hopefully I may be forgiven if I haven’t quite understood something fully.</p>
<h3 id="heading-the-context-window">The Context Window</h3>
<p>I understand the context window to work like the LLM's short-term memory. Our interface with a LLM is sending and receiving tokens; chunks of information that the LLM processes. The data could be text, audio, parts of an image, etc. The tokens that are sent and received are added to this context window and each interaction builds upon this.</p>
<p>$$context\_window = context\_window + new\_tokens$$</p><p>When you interact with an LLM through chat, the model doesn't actually remember previous messages in the conversation. Instead, the entire conversation and your new message are sent to the LLM each time. The model "remembers" by having this contextual information packaged with the new input. I was quite surprised by this, but I think it makes sense. It also explains why LLMs might have trouble with a sudden change of topic and why it's better to start a new chat. If you don't, the input for your next query will be cluttered with the context from your previous unrelated conversation.</p>
<h3 id="heading-pre-training">Pre-Training</h3>
<p>Pre-training is a complex and resource-intensive process that involves gathering vast amounts of text data and refining it into a model that can recognise and generate patterns. Rather than simply compressing knowledge, the model learns statistical relationships between words, enabling it to predict the next token in a sequence. While this can create the illusion of understanding, the model does not "remember" or "reason" in the way humans do—it recognises patterns based on probability rather than true comprehension. As a result, it excels at recalling frequently encountered information but struggles with rare or nuanced details, much like how humans forget infrequent experiences over time.</p>
<p>As pre-training is so expensive, it is done infrequently. This means that the model will have something called a knowledge cutoff. For example, if a model was pre-trained in November 2024, it will have no ‘knowledge’ about the events or information on the internet after this. You therefore cannot ask a LLM product (that relies solely on its model’s pre-training) to answer questions about current events.</p>
<h3 id="heading-post-training">Post-Training</h3>
<p>The post-training phase of a large language model (LLM) refines its capabilities beyond the initial pre-training stage. Its where each LLM product gets its unique traits. This phase typically includes fine-tuning and reinforcement learning from human feedback (RLHF) to align the model’s responses with human values, improve accuracy, and reduce biases. RLHF uses human annotators to rank model responses, guiding the model toward more helpful, coherent, and ethical outputs. Post-training also involves safety measures, such as filtering harmful content and mitigating biases, making the model more reliable for real-world applications.</p>
<p>Unlike pre-training, which is computationally intensive and infrequent, post-training can be done more regularly to adapt the model to user needs and evolving requirements.</p>
<h3 id="heading-reinforcement-learning-thinking">Reinforcement Learning - Thinking</h3>
<p>Thinking models are further refined through reinforcement learning, allowing them to develop more sophisticated "thinking strategies" that enhance their problem-solving abilities. This process involves iterative feedback loops, where the model evaluates different reasoning paths and learns to prioritise the most effective approaches. As a result, these models demonstrate significant improvements in complex tasks such as mathematics, coding, and logical reasoning. By continuously optimising their decision-making processes, they become more adept at breaking down problems, identifying patterns, and generating precise, well-structured solutions.</p>
<h3 id="heading-accessing-tools">Accessing Tools</h3>
<p>One limitation of LLMs is their knowledge cutoff, meaning they lack awareness of recent events and updates beyond their training data. Additionally, they may struggle with niche or less prominent information that wasn’t widely covered during training.</p>
<p>A solution to this is equipping LLMs with tools that allow them to retrieve real-time information. For example, integrating an internet search tool enables the LLM to pull in the latest search results, incorporating them into its context window (working memory) to generate more accurate and relevant responses.</p>
<p>When using a large language model (LLM) for maths, it’s important to be sceptical of its answers. While LLMs can recall basic mathematical facts, they do not inherently perform calculations or verify their outputs. Instead, they generate responses based on patterns, sometimes producing numbers that seem plausible but are actually incorrect—this is known as hallucination.</p>
<p>To overcome this limitation, LLMs can be equipped with tools such as a Python interpreter. If the model can recognise when it should rely on such a tool and effectively use it to compute the answer, the result is far more reliable. By offloading mathematical operations to a dedicated computation engine, we get accurate and verifiable solutions to mathematical problems.</p>
<p>This, once again, feels quite human. Just as we recognise and offload challenging tasks that we deem to be beyond our remit to tools. So too can a LLM source answers from more robust sources.</p>
<p>Outside of the video, I've been following the ‘buzz’ around the Model Context Protocol (MCP), which was open-sourced by Anthropic in 2024. Anthropic describes MCP as a “universal, open standard for connecting AI systems with data sources, replacing fragmented integrations with a single protocol.” Essentially, it aims to standardise how large language models (LLMs) interact with external tools, data sources, and services.</p>
<p>The idea behind MCP is to create a seamless framework that allows LLMs to access a variety of tools (like the internet, a Python interpreter, or other external APIs) in a consistent way. This would streamline integrations and make it easier for developers to connect LLM-based products with diverse data sources and services without having to build custom solutions for each one.</p>
<p>There are already MCP registries, such as <a target="_blank" href="https://smithery.ai/">Smithery</a>, which aggregate and provide a pool of integrations for various tools. This creates an ecosystem where LLMs can easily interface with tools they otherwise might not be able to access natively.</p>
<h3 id="heading-vibe-coding-with-cursor">Vibe Coding with Cursor</h3>
<p>This is the first time I was exposed to the <a target="_blank" href="https://www.cursor.com/">Cursor</a> text editor. It looks like a fork of <a target="_blank" href="https://code.visualstudio.com/">VSCode</a> that has a bounty of features that allows you to make effective use of AI in programming.</p>
<p>I was most interested in the example that Andrej gave of developing a tic-tac-toe game using ‘vibe coding’. Here no coding was done by Andrej, and exclusive control (in this case) was given to the text editor and AI. I will admit, it was quite scary to see the <a target="_blank" href="https://youtu.be/EWvNQjAaOHw?t=4907">impressive result</a> - a fully functioning browser-based game with neat animations and sounds.</p>
<p>This is definitely something that I would like to explore more, the only thing that I am uncertain of is the cost of one of these programming sessions. I am yet to find an example that gives an impression of this. I imagine that it would not be extortionate.</p>
<h2 id="heading-final-thoughts">Final Thoughts</h2>
<p>The video has given me a great sense of the various ways I could utilise LLMs in my day-to-day life, and has enhanced my overall understanding of how these models work. Going forward, I’d like to explore more with the Cursor text editor and give vibe coding a try, provided the cost isn’t prohibitively expensive for simple exploration.</p>
]]></content:encoded></item><item><title><![CDATA[Bitwise Operations in a Domain-Specific Language]]></title><description><![CDATA[I have been working with several devices controlled by a programmable logic controller (PLC). The PLC shows the state of these devices as an unsigned 16-bit integer. The integer value itself isn't very helpful and can even be a bit confusing. Each of...]]></description><link>https://gregorsoutar.com/bitwise-operations-in-a-domain-specific-language</link><guid isPermaLink="true">https://gregorsoutar.com/bitwise-operations-in-a-domain-specific-language</guid><category><![CDATA[bitwise operators]]></category><dc:creator><![CDATA[Gregor Soutar]]></dc:creator><pubDate>Fri, 07 Mar 2025 22:45:28 GMT</pubDate><content:encoded><![CDATA[<p>I have been working with several devices controlled by a programmable logic controller (PLC). The PLC shows the state of these devices as an unsigned 16-bit integer. The integer value itself isn't very helpful and can even be a bit confusing. Each of the 16 bits reflect whether the device is in a specific state. For example, if the device is a heater and bit 16 is on (1), it signals an alarm. If bit 2 is on (1), it may indicate that the heater is active.</p>
<p>My aim was to read those 16 bits for each device from the PLC using a domain-specific language as part of a wider piece of legacy software. Once I was able to read-in the value as an integer, I had to find a way to parse the information so that I could determine the state of each individual bit.</p>
<p>As someone who has rarely used bitwise operations, I probably would have come up with a solution like this (if using Python) that doesn’t use bitwise operations at all. It solves the issue with string operations.</p>
<pre><code class="lang-python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">is_bit_on</span>(<span class="hljs-params">data, bit</span>) -&gt; bool:</span>
    <span class="hljs-comment"># Convert to binary, pad, and get the bit of interest</span>
    <span class="hljs-keyword">return</span> bin(data)[<span class="hljs-number">2</span>:].zfill(<span class="hljs-number">16</span>)[bit] == <span class="hljs-string">"1"</span>

<span class="hljs-comment"># Example: 32768 (1000000000000000 in binary)</span>
print(is_bit_on(<span class="hljs-number">32768</span>, <span class="hljs-number">0</span>)) <span class="hljs-comment"># Output: True</span>
print(is_bit_on(<span class="hljs-number">32768</span>, <span class="hljs-number">1</span>)) <span class="hljs-comment"># Output: False</span>
</code></pre>
<p>Bitwise operations were not really on my radar, but there was certainly no way to do complex string operations. Delving in to the documentation I could see that the options I had available to me to process the integer were:</p>
<ul>
<li><p><strong>IF</strong>(&lt;condition&gt;, &lt;expr1&gt;, &lt;expr2&gt;)</p>
</li>
<li><p><strong>BITAND</strong>(&lt;int1&gt;, &lt;int2&gt;,…, &lt;intn&gt;)</p>
</li>
<li><p><strong>BITOR</strong>(&lt;int1&gt;, &lt;int2&gt;,…, &lt;intn&gt;)</p>
</li>
<li><p><strong>BITXOR</strong>(&lt;int1&gt;, &lt;int2&gt;,…, &lt;intn&gt;)</p>
</li>
<li><p><strong>BITLSHIFT</strong>(&lt;int1&gt;, &lt;int2&gt;)</p>
</li>
<li><p><strong>BITRSHIFT</strong>(&lt;int1&gt;, &lt;int2&gt;)</p>
</li>
</ul>
<p>A wild idea would be to list all the integers where each bit of interest is on and check for them in an IF statement. For example, if you are interested in the most significant bit (as we were), this would be all numbers from 32,768 to 65,535. This is clearly impractical, and the length of expressions (like the IF statement) is limited to 2048 characters in this proprietary language. We need another way forward.</p>
<p>Admittedly I did not come to the solution by myself. After doing some research into other devices and how they might do this already, one had the answer. After much staring I now understand how this neat little bit of code works to give us what we need.</p>
<pre><code class="lang-plaintext">ATTRIBUTE logical heaterAlarm 
BEGIN
    Definition IF(BITAND([&lt;uint16_value&gt;],0x8000) &gt; 0, 1, 0)
END
</code></pre>
<p>In the example above:</p>
<ul>
<li><p>An attribute of type logical is declared. An attribute of this type can be 1 or 0.</p>
</li>
<li><p>The value of this variable is then defined as the result of an <strong>IF</strong> expression; one of our limited set of options. The result of this expression must be 1 or 0 for it to be stored in an attribute of type logical.</p>
</li>
<li><p>The <strong>BITAND</strong> (bitwise AND) operation, when applied to two numbers, returns 1 for each bit if both bits are 1; otherwise, it returns 0.</p>
</li>
<li><p>0×8000 is the hexadecimal representation of the binary number <code>1000 0000 0000 0000</code>. It is used here as a mask to isolate the most significant bit. If you were interested in, for example, the second most significant bit you would use the mask 0×4000 (<code>0100 0000 0000 0000</code>).</p>
</li>
<li><p>The value could be any unsigned 16-bit integer. Lets assume it is 32,769 or <code>1000 0000 0000 0001</code>.</p>
</li>
<li><p>The result of the <strong>BITAND</strong> operation will be 32,768 <code>1000 0000 0000 0000</code> and we have now isolated the bit of interest. The integer after this operation can only be 32,768 or 0.</p>
</li>
<li><p>We get our required 1 or 0 using the <strong>IF</strong> expression. If the result of the <strong>BITAND</strong> operation is greater than zero, then we know our bit of interest was on. Otherwise it was not.</p>
</li>
</ul>
<p>This approach effectively solves the problem and shows the power of bitwise operations. It has deepened my understanding and appreciation for their use in programming. I believe this experience also emphasises the importance of research. It's likely that someone else has faced the same problem and already solved it. You can spend a lot of time trying to create an original solution, or you can invest some time finding out how it has already been done.</p>
]]></content:encoded></item><item><title><![CDATA[Humble Py(thon)]]></title><description><![CDATA[I’ve been doing some Python re-learning. The specific course that I am working my way through starts from the very beginning - a very good place to start. This may feel pointless, but sometimes the material can raise features and concepts that I have...]]></description><link>https://gregorsoutar.com/humble-python</link><guid isPermaLink="true">https://gregorsoutar.com/humble-python</guid><category><![CDATA[modulus]]></category><dc:creator><![CDATA[Gregor Soutar]]></dc:creator><pubDate>Tue, 04 Mar 2025 19:28:01 GMT</pubDate><content:encoded><![CDATA[<p>I’ve been doing some Python re-learning. The <a target="_blank" href="https://edube.org/study/pe2">specific course</a> that I am working my way through starts from the very beginning - a very good place to start. This may feel pointless, but sometimes the material can raise features and concepts that I have forgotten about, or not made the most of. Indeed, this happened today through one of the exercises in this course.</p>
<h2 id="heading-the-exercise">The Exercise</h2>
<p><strong>“</strong>Write a simple program that calculates the end time after a given number of minutes. The start time is provided as hours (0 to 23) and minutes (0 to 59). For example, if an event begins at 12:17 and lasts for 59 minutes, it will finish at 13:16.<strong>”</strong></p>
<p>The exercise gave the hint to use the <strong>modulus operator</strong> <code>%</code> to solve the problem.</p>
<p>After some time, and some internal debate surrounding the exercise being labeled as <strong>easy</strong>, and a small identity crisis on being a professional software engineer, I arrived at a solution. Easy!</p>
<pre><code class="lang-python">start_hour = int(input(<span class="hljs-string">"Enter start hour (0-23): "</span>))
start_minute = int(input(<span class="hljs-string">"Enter start minute (0-59): "</span>))
duration_minutes = int(input(<span class="hljs-string">"Enter duration in minutes: "</span>))

total_minutes = start_hour * <span class="hljs-number">60</span> + start_minute + duration_minutes

end_hour = (total_minutes // <span class="hljs-number">60</span>) % <span class="hljs-number">24</span>  
end_minute = total_minutes % <span class="hljs-number">60</span>

print(<span class="hljs-string">f"<span class="hljs-subst">{end_hour}</span>:<span class="hljs-subst">{end_minute}</span>"</span>)
</code></pre>
<h2 id="heading-utility-of-the-modulus-operator">Utility of the Modulus Operator</h2>
<p>The modulus operator is not unique to Python; it just so happens that this exercise was done in Python. My new-found respect for this operator resulted in a short investigation into its potential uses. I’ve summarised a few that I came across here:</p>
<ul>
<li><p><strong>Checking for even or odd numbers</strong></p>
<ul>
<li><p><code>if num % 2 == 0:</code> (even number)</p>
</li>
<li><p><code>if num % 2 != 0:</code> (odd number)</p>
</li>
</ul>
</li>
<li><p><strong>Looping with wraparounds</strong></p>
<ul>
<li><code>index = (index + 1) % len(arr)</code></li>
</ul>
</li>
<li><p><strong>Task scheduling (e.g. do something every 5 iterations)</strong></p>
<ul>
<li><code>if iteration % 5 == 0:</code></li>
</ul>
</li>
<li><p><strong>Divisibility tests (e.g. checking if a number is divisible by 3)</strong></p>
<ul>
<li><code>if num % 3 == 0:</code></li>
</ul>
</li>
<li><p><strong>Extracting digits from a number</strong></p>
<ul>
<li><code>last_digit = num % 10</code></li>
</ul>
</li>
</ul>
<p>Thanks to this humbling exercise, I am now more aware of this operator and its usefulness. I think this is a good example of how revisiting the basics can be quite helpful. When writing code to solve future problems, I will hopefully recognise and use the modulus operator when appropriate.</p>
]]></content:encoded></item></channel></rss>