<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom"><title>Andrew Wegner | Ponderings of an Andy - technical</title><link href="https://andrewwegner.com/" rel="alternate"/><link href="https://andrewwegner.com/feeds/tag/technical.atom.xml" rel="self"/><id>https://andrewwegner.com/</id><updated>2026-04-06T10:30:00-05:00</updated><subtitle>Can that be automated?</subtitle><entry><title>Review of Claude Code for Python Developers from Real Python</title><link href="https://andrewwegner.com/real-python-claude-code-live-course.html" rel="alternate"/><published>2026-04-06T10:30:00-05:00</published><updated>2026-04-06T10:30:00-05:00</updated><author><name>Andy Wegner</name></author><id>tag:andrewwegner.com,2026-04-06:/real-python-claude-code-live-course.html</id><summary type="html">&lt;p&gt;A review of the Real Python live workshop - Claude Code for Python Developers: Hands-on agentic coding course&lt;/p&gt;</summary><content type="html">
&lt;h2 id="introduction"&gt;Introduction&lt;a class="headerlink" href="#introduction" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;If you are in the software industry and haven't started working with an AI assistant by this point in 2026, then you should start getting concerned about your role in the company you work at. &lt;a href="https://www.cfodive.com/news/ai-tied-a-quarter-us-layoffs-march/816519/"&gt;Throughout the first few months of 2026&lt;/a&gt;, there have been several large layoffs by major corporations. &lt;a href="https://programs.com/resources/ai-layoffs/"&gt;Companies like Block and Oracle cited AI as the driver&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;AI is here and as of today, it's the least capable it will ever be, because each day it gets better. I've been exploring it since ChatGPT came out and it's &lt;a href="https://andrewwegner.com/ai-broke-our-interview-process-i-had-to-fix-it.html"&gt;impact on interviews&lt;/a&gt;. I've written about it multiple times recently. &lt;a href="https://andrewwegner.com/junior-engineer-crisis-ai-code-generation.html"&gt;AI's impact on junior developers&lt;/a&gt; is particularly important because I'm already seeing this in my role. &lt;/p&gt;
&lt;p&gt;My teams have been utilizing a handful of AI assistants over the past year and making amazing improvements to our workflows. From things as simple as reducing the SDLC cycle time for major features to triage of support items, the impact has been dramatic. &lt;/p&gt;
&lt;p&gt;I feel like I've only &lt;em&gt;touched&lt;/em&gt; the surface - not even scratched it - touched the surface of how to better use AI tooling, and I've been using it a lot. So, I looked for a workshop to increase my knowledge. As a subscriber to &lt;a href="https://realpython.com/"&gt;Real Python&lt;/a&gt;, I was happy to see they offered a brand new workshop about &lt;a href="https://code.claude.com/docs/en/overview"&gt;Claude Code&lt;/a&gt; and as of this writing they are offering it again.&lt;/p&gt;
&lt;p&gt;The course - &lt;a href="https://realpython.com/workshops/claude-code/"&gt;Claude Code for Python Developers: Hands-On Agentic Coding Course&lt;/a&gt;. A bit wordy, but an amazing two days.&lt;/p&gt;
&lt;h2 id="spoiler"&gt;Spoiler&lt;a class="headerlink" href="#spoiler" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;As a spoiler, my review of the course is on the main course page. &lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"Learning about how Claude Code works was great. Working with things like Skills, learning a workflow that functions, was what I was hoping to learn about. All of those were covered."&lt;/p&gt;
&lt;p&gt;"I feel more comfortable with the tool itself and how to implement a basic workflow for myself with ideas on how to extend it to a whole team."&lt;/p&gt;
&lt;p&gt;"This is one of the best training sessions I've joined in the last year across multiple platforms."&lt;/p&gt;
&lt;p&gt;— Andrew Wegner, VP Product at Zayo&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img alt="My review of the Real Python Claude Code workshop that I endorse" src="https://andrewwegner.com/images/real-python-claude-code-endoursement.png"/&gt;&lt;/p&gt;
&lt;h2 id="course"&gt;Course&lt;a class="headerlink" href="#course" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;As you can see from above, I thought this course was great. It was a two day (over a weekend) live course with about 100 participants from around the world. Based on the chat during the course, the skill set of participants ranged from "new to Claude code but experienced developer" to "somewhat familiar with Claude Code and looking to do more with it". It was a nice mix of participants. The instructor, &lt;a href="https://realpython.com/team/pacsany/"&gt;Philipp Acsany&lt;/a&gt;, did a good job of answering questions, sharing content, and ensuring everyone was able to follow along. This was a very hands on workshop.&lt;/p&gt;
&lt;p&gt;Before the course began, set up instructions were sent out. I can not overstate how much this was appreciated, because that meant the course could assume that everyone has a functioning environment to work in. This saved so much time on basic questions and allowed the course to start with the interesting content, not a tutorial on how to install Claude Code, GitHub CLI, Python and uv, Git and Zoom.&lt;/p&gt;
&lt;p&gt;Day 1 started with the very basics of Claude. Normally, I'd be annoyed by starting with such a basic concept, but Philipp kept it engaging and more importantly, showed some best practices using Claude to scaffold a new project that I hadn't seen. Thinking that this bodes well for the rest of the course, I eagerly followed along. &lt;/p&gt;
&lt;p&gt;As Day 1 moved on, we built upon our scaffold to develop a small application, learning how to manipulate various aspects of claude, setting up prompts and skills to assist our workflow and learning how to debug when something doesn't work. Day 1 concluded with a little bit of homework. After 4 hours in the workshop, I felt pretty confident that I could accomplish this and was happy to see that confidence was justified. After about an hour more of individual work, I completed the tasks and was ready for day 2.&lt;/p&gt;
&lt;p&gt;Day two built on top of the homework by adding in additional features, ensuring we were able to utilize various skills, and learning more about how Claude operates under the hood. The session concluded with quick demos of additional aspects of Claude - hooks, MCPs, and Agents. Honestly, this was my biggest disappointment in the course, because it showed so many things we wouldn't be getting to, but it did fill my "research later" queue.&lt;/p&gt;
&lt;h2 id="conclusion"&gt;Conclusion&lt;a class="headerlink" href="#conclusion" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;This isn't a cheap course. But, it was worth it to me. I approached this with two goals in mind:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Learn something for myself that I could take and apply to personal projects&lt;/li&gt;
&lt;li&gt;Learn something for my teams so that we could use it as inspiration to make further improvements to our workflows&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Both of these were met in Day 1. That made Day 2 even more fun for me, because I showed up wanting to learn more, cover more, do more. As I said in my review on Real Python: This is one of the best training sessions I've joined in the last year across multiple platforms. &lt;/p&gt;
&lt;p&gt;Through out the course, student questions were responded to - both live via Philipp and in chat via one of Philipp's partners or from other participants. I found this aspect of the course really valuable too. I did get a handful of questions answered during it, but I was able to provide answers as well. &lt;/p&gt;
&lt;p&gt;So, why did I only give this a 9 out of 10? What's preventing that last star?&lt;/p&gt;
&lt;p&gt;There are aspects of the course that were touched on so briefly that I think would have been useful to dive into. These are the topics that have ended up in my research queue - hooks, mcp services, agents, agent teams. I think these could have filled another 4 hour block, but this was a weekend course and didn't fit. I'll be watching for a session that covers these topics.&lt;/p&gt;
&lt;p&gt;The other thing - the cost can be prohibitive to students. There is the cost of the course itself: $800 normally, $500 on sale when I joined. Plus the cost of Claude. It is recommended to get at least the Max plan which runs $100 per month. I agree with that. I think if I'd only gone with Pro, I'd have hit usage limits during the course.&lt;/p&gt;
&lt;p&gt;That said, if you can afford it (or get work to cover it as training), this is worth it for both an introduction to Claude Code and to learn about features you likely aren't using to their full power. Even with this course, I don't think I am doing that yet, but I know what to research now to get better for personal usage and for team improvements.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://realpython.com/certificates/2e9b80f4-03f7-4a89-b9ef-7fef48829b8c/"&gt;&lt;img alt="Real Python Claude Code for Python Developers: Hands-on Agentic Coding certificate of completion" src="https://andrewwegner.com/images/real-python-claude-code-certificate.png"/&gt;&lt;/a&gt;&lt;/p&gt;</content><category term="Review"/><category term="review"/><category term="technical"/><category term="learning"/></entry><entry><title>Python Gotcha: Reusing Generators Returns Nothing</title><link href="https://andrewwegner.com/python-gotcha-reusing-generator-returns-nothing.html" rel="alternate"/><published>2025-07-22T09:00:00-05:00</published><updated>2025-07-22T09:00:00-05:00</updated><author><name>Andy Wegner</name></author><id>tag:andrewwegner.com,2025-07-22:/python-gotcha-reusing-generator-returns-nothing.html</id><summary type="html">&lt;p&gt;Generators provide lazy evaluation for processing large datasets efficiently. However, once a generator is exhausted through iteration, it cannot be reused or reset. Let's cover this common gotcha that trips up developers new to this Python feature.&lt;/p&gt;</summary><content type="html">
&lt;h2 id="introduction"&gt;Introduction&lt;a class="headerlink" href="#introduction" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;In the previous article, we looked at &lt;a href="https://andrewwegner.com/python-gotcha-logging-uncaught-exception.html"&gt;logging uncaught exceptions&lt;/a&gt;. Let's utilize the log output from that post for another common task: error log file processing. This example is going to be pretty simple, as the error log for the post is tiny, but in a production environment this could be hundreds of thousands of lines, or gigabytes in size. Potentially, that's a lot to shove into memory for processing. But that's where a generator can come in to help.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://docs.python.org/3/tutorial/classes.html#generators"&gt;Generators&lt;/a&gt; do not store their results, instead they maintain state and &lt;code&gt;yield&lt;/code&gt; the result back to the caller. This means each line in a log can be processed and returned, without loading the entire file. &lt;/p&gt;
&lt;h3 id="flashback"&gt;Flashback&lt;a class="headerlink" href="#flashback" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;As a reminder from the &lt;a href="https://andrewwegner.com/python-gotcha-logging-uncaught-exception.html"&gt;previous article&lt;/a&gt;, the log file being used looks like this:&lt;/p&gt;
&lt;div class="codehilight code"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="mf"&gt;2025&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;07&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;14&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;22&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;30&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;44&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;061&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;__main__&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;INFO&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Application&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt;
&lt;span class="mf"&gt;2025&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;07&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;14&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;22&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;30&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;44&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;061&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;__main__&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;CRITICAL&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;uncaught&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;exception&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;application&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;will&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;terminate&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;
&lt;span class="n"&gt;Traceback&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;most&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;recent&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;last&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;span class="n"&gt;File&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"/home/andy/main.py"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;31&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;module&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="err"&gt;~~~~&lt;/span&gt;&lt;span class="o"&gt;^^&lt;/span&gt;
&lt;span class="n"&gt;File&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"/home/andy/main.py"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;27&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;main&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nb"&gt;log&lt;/span&gt;&lt;span class="n"&gt;ger&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;divide&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="err"&gt;~~~~~~&lt;/span&gt;&lt;span class="o"&gt;^^^^^&lt;/span&gt;
&lt;span class="n"&gt;File&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"/home/andy/main.py"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;21&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;divide&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kr"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="err"&gt;~&lt;/span&gt;&lt;span class="o"&gt;^&lt;/span&gt;&lt;span class="err"&gt;~&lt;/span&gt;
&lt;span class="n"&gt;ZeroDivisionError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;division&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;by&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;zero&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;There is one &lt;code&gt;INFO&lt;/code&gt; entry and one &lt;code&gt;CRITICAL&lt;/code&gt; entry.&lt;/p&gt;
&lt;h2 id="yield-vs-return"&gt;yield vs return&lt;a class="headerlink" href="#yield-vs-return" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;It's important to understand what makes a generator different from a regular function. The key distinction is the &lt;code&gt;yield&lt;/code&gt; keyword. When a function contains &lt;code&gt;yield&lt;/code&gt;, Python treats it as a generator function, which behaves differently from functions that use &lt;code&gt;return&lt;/code&gt;. &lt;/p&gt;
&lt;p&gt;A regular function with &lt;code&gt;return&lt;/code&gt; executes completely, returns back a single result and then terminates. A function with &lt;code&gt;yield&lt;/code&gt; creates a generator object that can pause execution, return a value, and later resume from exactly where it left off. This is what enables the memory-efficient and lazy evaluation that makes generators powerful.&lt;/p&gt;
&lt;h2 id="gotcha"&gt;Gotcha&lt;a class="headerlink" href="#gotcha" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;h3 id="log-processing"&gt;Log processing&lt;a class="headerlink" href="#log-processing" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;A generator result can only be utilized one time. Whether you are using a generator to output the next item in a sequence or process a file line by line, once you have passed an iterable or exhausted the generator, it doesn't get reused. This simple generator demonstrates the issue.&lt;/p&gt;
&lt;div class="codehilight code"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;def read_log_lines(filename):
    with open(filename, 'r') as f:
        for line in f:
            if 'CRITICAL' in line:
                yield line.strip()

error_logs = read_log_lines('app.log')

error_count = len(list(error_logs))
print(f"Found {error_count} CRITICAL lines")

recent_errors = [log for log in error_logs if '2025' in log]
print(f"Recent errors: {len(recent_errors)}")
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;At first glance, it looks like this will read the log file, count the number of errors and then output how many of those were in 2025 (or contain the string &lt;code&gt;2025&lt;/code&gt;). However, the actual output is different.&lt;/p&gt;
&lt;div class="codehilight code"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;Found 1 CRITICAL lines
Recent errors: 0
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code&gt;error_logs&lt;/code&gt; is a generator object. If a function &lt;code&gt;yield&lt;/code&gt;s, it is a generator. As &lt;code&gt;error_count&lt;/code&gt; is initialized, it processes the error log and yields back any critical lines. The &lt;code&gt;list()&lt;/code&gt; function will consume the entire generator (file). A few lines later, the developer wants to see how many of these are recent errors and attempts to go through the &lt;code&gt;error_logs&lt;/code&gt; generator again. Success! No recent errors!&lt;/p&gt;
&lt;p&gt;Right?&lt;/p&gt;
&lt;p&gt;No, and looking at the log quickly shows that.&lt;/p&gt;
&lt;h3 id="fibonacci"&gt;Fibonacci&lt;a class="headerlink" href="#fibonacci" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Let's use a generator to build the Fibonacci sequence. Spoiler for interviews! In this case, I'm going to use a generator to get the first 10 items. Then print out the first 5 and then try to print the entire list of 10 items.&lt;/p&gt;
&lt;div class="codehilight code"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;fibonacci&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nb"&gt;yield&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;

&lt;span class="n"&gt;fib_numbers&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;fibonacci&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"First 5 numbers:"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;num&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fib_numbers&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;num&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="k"&gt;break&lt;/span&gt;

&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Entire List:"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;full_list&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fib_numbers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;full_list&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The output for this is:&lt;/p&gt;
&lt;div class="codehilight code"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;First 5 numbers:
0
1
1
2
3
Entire List:
[5, 8, 13, 21, 34]
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Notice that the &lt;code&gt;full_list&lt;/code&gt; variable only contains the items remaining on the generator. Since the first 5 (indexes &lt;code&gt;0&lt;/code&gt; through &lt;code&gt;4&lt;/code&gt;) were printed, they are no longer part of the generator. When the full list is printed, only the remaining items can be printed.&lt;/p&gt;
&lt;h2 id="solution"&gt;Solution&lt;a class="headerlink" href="#solution" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The solution to the problem is easy enough. Call the generator function again. For example, with the log code from above:&lt;/p&gt;
&lt;div class="codehilight code"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;error_logs = read_log_lines('app.log')
error_count = len(list(error_logs))
...
error_logs = read_log_lines('app.log')  # Call again and create a new generator
recent_errors = [log for log in error_logs if '2025' in log]
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;For the Fibonacci code, you would call &lt;code&gt;fib_numbers = fibonacci(10)&lt;/code&gt; again before printing the full list.&lt;/p&gt;
&lt;p&gt;Obviously, there is a down side here with duplicate processing of the same data due to running the generator twice. This could probably be solved with some logic adjustments to the generator or the code calling the generator, but that'll vary by application depending on what the generator is doing.&lt;/p&gt;
&lt;h3 id="conclusion"&gt;Conclusion&lt;a class="headerlink" href="#conclusion" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;The important thing to take away from this is that once you have iterated over an item in a generator, it's no longer part of a generator. This means that if you want to get clever and see if there are more items in a generator, or determine the next item, you've consumed the next item.&lt;/p&gt;
&lt;p&gt;The power of generators, especially when processing large amounts of data, can't be understated. But, at the same time, it's important to know that reusing an exhausted generator or attempting to access a previous item directly from the generator is not going to work. Instead, to reuse generator logic, call the generator function again to create a new generator object, or convert to a list if memory permits and multiple iterations are needed.&lt;/p&gt;</content><category term="Technical Solutions"/><category term="technical"/></entry><entry><title>Python Gotcha: Logging an uncaught exception</title><link href="https://andrewwegner.com/python-gotcha-logging-uncaught-exception.html" rel="alternate"/><published>2025-07-14T23:00:00-05:00</published><updated>2025-07-14T23:00:00-05:00</updated><author><name>Andy Wegner</name></author><id>tag:andrewwegner.com,2025-07-14:/python-gotcha-logging-uncaught-exception.html</id><summary type="html">&lt;p&gt;Uncaught exceptions will crash an application. If you don't know how to log these, it can be difficult to troubleshoot such a crash. Let's walk through this gotcha and see how to fix it.&lt;/p&gt;</summary><content type="html">
&lt;h2 id="introduction"&gt;Introduction&lt;a class="headerlink" href="#introduction" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;A well built application will use logging instead of &lt;code&gt;print&lt;/code&gt; statements. An exceptionally well built one will log in such a way that additional context is added to each log message and be consumable by a log aggregation service. Perhaps I'll write up such an article in the future. For now though, let's focus on a single problem. &lt;/p&gt;
&lt;p&gt;Here is some sample code to demonstrate the problem.&lt;/p&gt;
&lt;div class="codehilight code"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;logging&lt;/span&gt;

&lt;span class="n"&gt;logger&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;getLogger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vm"&gt;__name__&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;setLevel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;INFO&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;handler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FileHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"app.log"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;formatter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Formatter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;%(asctime)s&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="si"&gt;%(name)s&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="si"&gt;%(levelname)s&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="si"&gt;%(message)s&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;handler&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;setFormatter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;formatter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;addHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;divide&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Application start"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;
    &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;divide&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Application end"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="vm"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"__main__"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Briefly, this sets up the &lt;code&gt;app.log&lt;/code&gt; file to receive our log messages. The &lt;code&gt;main&lt;/code&gt; function is going to divide two numbers, log the result, and end the program. Pretty simple.&lt;/p&gt;
&lt;p&gt;Except, in this case, it is dividing by zero. This throws an error and crashes the program.&lt;/p&gt;
&lt;p&gt;The console spits out a stack trace&lt;/p&gt;
&lt;div class="codehilight code"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nv"&gt;Traceback&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;most&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;recent&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;call&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;last&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;:
&lt;span class="nv"&gt;File&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/home/andy/main.py"&lt;/span&gt;,&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;line&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;22&lt;/span&gt;,&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nv"&gt;module&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nv"&gt;main&lt;/span&gt;&lt;span class="ss"&gt;()&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="o"&gt;~~~~^^&lt;/span&gt;
&lt;span class="nv"&gt;File&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/home/andy/main.py"&lt;/span&gt;,&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;line&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;18&lt;/span&gt;,&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;main&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nv"&gt;logger&lt;/span&gt;.&lt;span class="nv"&gt;info&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;divide&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;a&lt;/span&gt;,&lt;span class="nv"&gt;b&lt;/span&gt;&lt;span class="ss"&gt;))&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="o"&gt;~~~~~~^^^^^&lt;/span&gt;
&lt;span class="nv"&gt;File&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/home/andy/main.py"&lt;/span&gt;,&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;line&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;,&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;divide&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;a&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nv"&gt;b&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="o"&gt;~^~&lt;/span&gt;
&lt;span class="nv"&gt;ZeroDivisionError&lt;/span&gt;:&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;division&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;by&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;zero&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The &lt;code&gt;app.log&lt;/code&gt; file contains a single line:&lt;/p&gt;
&lt;div class="codehilight code"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="mf"&gt;2025&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;07&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;14&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;22&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;20&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;04&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;551&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;__main__&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;INFO&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Application&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2 id="gotcha"&gt;Gotcha&lt;a class="headerlink" href="#gotcha" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Where is the gotcha here? The stack trace is right there!&lt;/p&gt;
&lt;p&gt;You are, of course, right. However, imagine that this was not a simple application, but instead a production application that sends logs to a central service. Your application crashed and no one was watching the console. Your &lt;code&gt;app.log&lt;/code&gt; file has no information. It says the application started and then...nothing. What happened? Is it still running?&lt;/p&gt;
&lt;p&gt;As you dig through running processes, or check a &lt;code&gt;/health&lt;/code&gt; end point for responses, you find out that it isn't running. That took a lot of time, and production isn't responding.&lt;/p&gt;
&lt;p&gt;You've lost all visibility to what happened in your application at the most critical moment. When it crashed and spit out a stack trace, you want as much detail as you can get.&lt;/p&gt;
&lt;h2 id="solution"&gt;Solution&lt;a class="headerlink" href="#solution" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The solution is &lt;a href="https://docs.python.org/3/library/sys.html#sys.excepthook"&gt;sys.excepthook&lt;/a&gt;. This is called when any exception is raised and uncaught, except for &lt;code&gt;SystemExit&lt;/code&gt;. It's pretty easy to utilize as well. A few small changes to the above code will allow us to log this completely unexpected &lt;code&gt;ZeroDivisionError&lt;/code&gt;. &lt;/p&gt;
&lt;div class="codehilight code"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;logging&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;sys&lt;/span&gt;

&lt;span class="n"&gt;logger&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;getLogger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vm"&gt;__name__&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;setLevel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;INFO&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;handler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FileHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"app.log"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;formatter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Formatter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;%(asctime)s&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="si"&gt;%(name)s&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="si"&gt;%(levelname)s&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="si"&gt;%(message)s&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;handler&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;setFormatter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;formatter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;addHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;handle_uncaught_exception&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exc_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;exc_value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;exc_traceback&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;critical&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s2"&gt;"uncaught exception, application will terminate."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;exc_info&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exc_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;exc_value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;exc_traceback&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;excepthook&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;handle_uncaught_exception&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;divide&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Application start"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;
    &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;divide&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Application end"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="vm"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"__main__"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The important bit is the new &lt;code&gt;handle_uncaught_exception&lt;/code&gt; function and the &lt;code&gt;sys.excepthook&lt;/code&gt; line (with appropriate &lt;code&gt;import&lt;/code&gt; statement). &lt;/p&gt;
&lt;p&gt;Someone running this in the console will notice that there is not a stack trace dumped to the console now. Instead, our log contains important information:&lt;/p&gt;
&lt;div class="codehilight code"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="mf"&gt;2025&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;07&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;14&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;22&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;30&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;44&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;061&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;__main__&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;INFO&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Application&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt;
&lt;span class="mf"&gt;2025&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;07&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;14&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;22&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;30&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;44&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;061&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;__main__&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;CRITICAL&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;uncaught&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;exception&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;application&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;will&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;terminate&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;
&lt;span class="n"&gt;Traceback&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;most&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;recent&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;last&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;span class="n"&gt;File&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"/home/andy/main.py"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;31&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;module&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="err"&gt;~~~~&lt;/span&gt;&lt;span class="o"&gt;^^&lt;/span&gt;
&lt;span class="n"&gt;File&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"/home/andy/main.py"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;27&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;main&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nb"&gt;log&lt;/span&gt;&lt;span class="n"&gt;ger&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;divide&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="err"&gt;~~~~~~&lt;/span&gt;&lt;span class="o"&gt;^^^^^&lt;/span&gt;
&lt;span class="n"&gt;File&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"/home/andy/main.py"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;21&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;divide&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kr"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="err"&gt;~&lt;/span&gt;&lt;span class="o"&gt;^&lt;/span&gt;&lt;span class="err"&gt;~&lt;/span&gt;
&lt;span class="n"&gt;ZeroDivisionError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;division&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;by&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;zero&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This is great! Now when troubleshooting this failing application and looking at the logs, we can easily see that an exception occurred. Additionally, with proper updates to the logging, more context can be provided such as the values of &lt;code&gt;a&lt;/code&gt; and &lt;code&gt;b&lt;/code&gt;. While it's easy enough to figure out that &lt;code&gt;b&lt;/code&gt; is &lt;code&gt;0&lt;/code&gt; in this simple example, the context in a larger production application could save a ton of troubleshooting time.&lt;/p&gt;
&lt;h2 id="conclusion"&gt;Conclusion&lt;a class="headerlink" href="#conclusion" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Logging is vital to knowing what your application is doing. But, it's even more important to determining why it stopped working. If your logs aren't providing information when the application crashes, it's functionally useless. By implementing an &lt;code&gt;excepthook&lt;/code&gt;, you can catch and properly log uncaught exceptions.&lt;/p&gt;
&lt;p&gt;I know some of you are coming up with alternatives. Terrible ideas like wrapping the entire main block in a &lt;code&gt;try/except&lt;/code&gt;. There are legitimate reasons to throw an exception. In this case, a &lt;code&gt;ZeroDivisionError&lt;/code&gt; is a great exception to catch. But, you'd want to do it around as small of a code block as possible.&lt;/p&gt;
&lt;p&gt;This is a clean way to catch truly unexpected exceptions, not something a developer could have anticipated and, perhaps, fixed with additional input validation.&lt;/p&gt;</content><category term="Technical Solutions"/><category term="technical"/></entry><entry><title>How to fix GitLab 18 error regarding git_data_dirs</title><link href="https://andrewwegner.com/gitlab_18_git_data_dirs_resolution.html" rel="alternate"/><published>2025-07-06T08:00:00-05:00</published><updated>2025-07-06T08:00:00-05:00</updated><author><name>Andy Wegner</name></author><id>tag:andrewwegner.com,2025-07-06:/gitlab_18_git_data_dirs_resolution.html</id><summary type="html">&lt;p&gt;GitLab 18 removes &lt;code&gt;git_data_dirs&lt;/code&gt; and if you have been using it and didn't notice the deprecation warnings, an update to GitLab 18 will fail. This is a simple fix.&lt;/p&gt;</summary><content type="html">
&lt;h2 id="introduction"&gt;Introduction&lt;a class="headerlink" href="#introduction" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Several years ago I &lt;a href="https://andrewwegner.com/installing-gitlab.html"&gt;set up GitLab in my home environment&lt;/a&gt;. I appreciate past me documenting the basic steps I took to do this, because I'll need them again at some point when I make upgrades to my home lab. I've documented some things I've done (like &lt;a href="https://andrewwegner.com/setting-up-gitlab-runners.html"&gt;setting up GitLab runners&lt;/a&gt;, or dealing with &lt;a href="https://andrewwegner.com/disable-grafana-in-gitlab-16.html"&gt;Grafana being deprecated within GitLab&lt;/a&gt; or utilizing &lt;a href="https://andrewwegner.com/obsidian-gitlab-setup.html"&gt;GitLab to automatically backup my Obsidian notes&lt;/a&gt;) Unfortunately, I didn't document everything. One of those things that I didn't document was changing where my repositories are stored on disk by default.&lt;/p&gt;
&lt;p&gt;In GitLab 17.8 (January 2025), a new deprecation warning started appearing.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;git_data_dirs has been deprecated since 17.8 and will be removed in 18.0. See https://docs.gitlab.com/omnibus/settings/configuration.html#migrating-from-git_data_dirs for migration instructions.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I'm a little annoyed that there were only 5 months of notice on this because major versions are released in May. In any case, if you attempt to update to version 18 or beyond and are utilizing &lt;code&gt;git_data_dirs&lt;/code&gt;, the upgrade will fail.&lt;/p&gt;
&lt;h2 id="solution"&gt;Solution&lt;a class="headerlink" href="#solution" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;As usual, GitLab is good at providing &lt;a href="https://docs.gitlab.com/omnibus/settings/configuration/#migrating-from-git_data_dirs"&gt;documentation on resolving and migrating&lt;/a&gt; through the deprecations. However, on my first attempt I encountered an error. I believe it's because I missed a note buried in the text - not the code blocks - the first time.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Note that the &lt;code&gt;/repositories&lt;/code&gt; suffix must be appended to the path because it was previously appended internally.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The solution is to open &lt;code&gt;/etc/gitlab/gitlab.rb&lt;/code&gt; (you probably should make a backup first). Find the &lt;code&gt;git_data_dirs&lt;/code&gt; line. For me this was around line 455. Then comment out this entire block.&lt;/p&gt;
&lt;p&gt;Then find (or add) the &lt;code&gt;gitaly['configuration']&lt;/code&gt; block. This was immediately after the &lt;code&gt;git_data_dirs&lt;/code&gt; section for me. Uncomment it, and add the appropriate path (from your &lt;code&gt;git_data_dirs&lt;/code&gt; block) and add &lt;code&gt;/repositories&lt;/code&gt; to the end of it.&lt;/p&gt;
&lt;p&gt;Once you are done, run&lt;/p&gt;
&lt;div class="codehilight code"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;gitlab-ctl reconfigure
gitlab-ctl restart
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Give it a minute to fire up all the GitLab subcomponents, and then you should be able to update GitLab beyond version 18.&lt;/p&gt;
&lt;h3 id="new-code-section"&gt;New code section&lt;a class="headerlink" href="#new-code-section" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;My &lt;code&gt;gitlab.rb&lt;/code&gt; now has this:&lt;/p&gt;
&lt;div class="codehilight code"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gh"&gt;#&lt;/span&gt;git_data_dirs({
&lt;span class="gh"&gt;#&lt;/span&gt;  "default" =&amp;gt; {
&lt;span class="gh"&gt;#&lt;/span&gt;    "path" =&amp;gt; "/previous/path/to/repos"
&lt;span class="gh"&gt;#&lt;/span&gt;   }
&lt;span class="gh"&gt;#&lt;/span&gt;})

&lt;span class="gu"&gt;##&lt;/span&gt;# Gitaly settings
gitaly['configuration'] = {
storage: [
    {
    name: 'default',
    path: '/previous/path/to/repos/repositories',
    },
],
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2 id="conclusion"&gt;Conclusion&lt;a class="headerlink" href="#conclusion" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;With this quick change, I can continue to hold the repositories at a location of my choosing. The biggest thing is to add &lt;code&gt;/repositories&lt;/code&gt; to the &lt;code&gt;path&lt;/code&gt; in the new Gitaly configuration. With this change, I can continue to utilize the current version of GitLab - a tool that I still find invaluable.&lt;/p&gt;</content><category term="Technical Solutions"/><category term="technical"/><category term="gitlab"/></entry><entry><title>Python Gotcha: Identity vs Equality - When 'is' Fails Unexpectedly</title><link href="https://andrewwegner.com/python-gotcha-identity-vs-equality.html" rel="alternate"/><published>2025-06-17T08:00:00-05:00</published><updated>2025-06-17T08:00:00-05:00</updated><author><name>Andy Wegner</name></author><id>tag:andrewwegner.com,2025-06-17:/python-gotcha-identity-vs-equality.html</id><summary type="html">&lt;p&gt;To a new developer &lt;code&gt;is&lt;/code&gt; can look like an equality check in Python, especially in poorly written tutorials. I'll give an overview of what &lt;code&gt;is&lt;/code&gt; is and how you should use it in only limited circumstances.&lt;/p&gt;</summary><content type="html">
&lt;h2 id="introduction"&gt;Introduction&lt;a class="headerlink" href="#introduction" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;In the &lt;a href="https://andrewwegner.com/python-gotcha-comparisons.html"&gt;comparisons gotcha&lt;/a&gt; I wrote a few years ago, I briefly touched on &lt;a href="https://andrewwegner.com/python-gotcha-comparisons.html#is-vs"&gt;&lt;code&gt;is&lt;/code&gt; vs &lt;code&gt;==&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Put simply, &lt;code&gt;is&lt;/code&gt; should ONLY be used if you are checking if two references refer to the same object.
&lt;em&gt;Remember, &lt;code&gt;is&lt;/code&gt; compares object references.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Even more simply, &lt;code&gt;is&lt;/code&gt; is &lt;em&gt;not&lt;/em&gt; checking value. Let's take a look at a couple examples.&lt;/p&gt;
&lt;h2 id="gotcha"&gt;Gotcha&lt;a class="headerlink" href="#gotcha" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;h3 id="integers"&gt;Integers&lt;a class="headerlink" href="#integers" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Python, specifically CPython, caches the values of &lt;code&gt;-5&lt;/code&gt; through &lt;code&gt;256&lt;/code&gt; (inclusive). This means that these small integer values will always refer to the same object. &lt;/p&gt;
&lt;p&gt;Note the phrasing there - "the same object".&lt;/p&gt;
&lt;p&gt;Outside of that range, though, the same is not true. &lt;/p&gt;
&lt;div class="codehilight code"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&amp;gt;&amp;gt;&amp;gt; a = 100
&amp;gt;&amp;gt;&amp;gt; b = 100
&amp;gt;&amp;gt;&amp;gt; a is b
True
&amp;gt;&amp;gt;&amp;gt; a = 257
&amp;gt;&amp;gt;&amp;gt; b = 257
&amp;gt;&amp;gt;&amp;gt; a is b
False
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;In both of the above examples, using &lt;code&gt;a == b&lt;/code&gt; would have returned &lt;code&gt;True&lt;/code&gt;. The mistake was assuming that &lt;code&gt;is&lt;/code&gt; does the same thing. It does not.&lt;/p&gt;
&lt;h3 id="strings"&gt;Strings&lt;a class="headerlink" href="#strings" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;String interning is a method of storing only one copy of each distinct immutable string value. Immutable strings can't be changed. Not every string will be interned though. Let's take a look:&lt;/p&gt;
&lt;div class="codehilight code"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&amp;gt;&amp;gt;&amp;gt; a = "Hello"
&amp;gt;&amp;gt;&amp;gt; b = "Hello"
&amp;gt;&amp;gt;&amp;gt; a is b
True
&amp;gt;&amp;gt;&amp;gt; a = "Hello World"
&amp;gt;&amp;gt;&amp;gt; b = "Hello World"
&amp;gt;&amp;gt;&amp;gt; a is b
False
&amp;gt;&amp;gt;&amp;gt; a = "Hello_World"
&amp;gt;&amp;gt;&amp;gt; b = "Hello_World"
&amp;gt;&amp;gt;&amp;gt; a is b
True
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The first and last example interned the strings, showing that &lt;code&gt;a&lt;/code&gt; and &lt;code&gt;b&lt;/code&gt; refer to the same object. But, the second example - &lt;code&gt;Hello World&lt;/code&gt; - didn't get interned, so &lt;code&gt;a&lt;/code&gt; and &lt;code&gt;b&lt;/code&gt; refer to different objects. Why is this?&lt;/p&gt;
&lt;p&gt;The short and simply answer is that any string that has only numbers, letters or underscores will be interned. Since &lt;code&gt;Hello World&lt;/code&gt; contains a &lt;code&gt;space&lt;/code&gt;, it would not be interned.&lt;/p&gt;
&lt;h2 id="the-solution"&gt;The Solution&lt;a class="headerlink" href="#the-solution" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;To a new developer that has seen tutorials that read &lt;code&gt;if a is True&lt;/code&gt; or &lt;code&gt;if b is None&lt;/code&gt;, a conditional for integers or strings following the same pattern &lt;em&gt;appears&lt;/em&gt; to be comparing values. If they test it with small, positive numbers or simple one word strings, the assumption holds up. &lt;/p&gt;
&lt;p&gt;But, &lt;code&gt;==&lt;/code&gt; is for comparing values! Each of the above examples would return &lt;code&gt;True&lt;/code&gt; by changing the statement to &lt;code&gt;a == b&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The few times that &lt;code&gt;is&lt;/code&gt; is appropriate are when you are checking &lt;code&gt;True&lt;/code&gt;/&lt;code&gt;False&lt;/code&gt; or &lt;code&gt;None&lt;/code&gt;. Otherwise, the &lt;em&gt;vast&lt;/em&gt; majority of the time, you want to use an equality check (&lt;code&gt;==&lt;/code&gt;)&lt;/p&gt;
&lt;p&gt;The Python &lt;a href="https://peps.python.org/pep-0008/#programming-recommendations"&gt;PEP8 programming recommendations&lt;/a&gt; state:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Comparisons to singletons like &lt;code&gt;None&lt;/code&gt; should always be done with &lt;code&gt;is&lt;/code&gt; or &lt;code&gt;is not&lt;/code&gt;, never the equality operators.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The linters in the Python ecosystem report on the usage of &lt;code&gt;is&lt;/code&gt; vs &lt;code&gt;==&lt;/code&gt; too. &lt;code&gt;flake8&lt;/code&gt; has &lt;a href="https://www.flake8rules.com/rules/E711.html"&gt;E711&lt;/a&gt; - &lt;code&gt;Comparison to None should be 'cond is None:'&lt;/code&gt;. &lt;code&gt;ruff&lt;/code&gt; has a similar report with it's &lt;a href="https://docs.astral.sh/ruff/rules/none-comparison/"&gt;&lt;code&gt;None&lt;/code&gt; comparison&lt;/a&gt; check.&lt;/p&gt;
&lt;p&gt;I highly recommend a linter for your projects to catch this, and other problems that go against best practices. &lt;/p&gt;
&lt;p&gt;&lt;em&gt;Remember, &lt;code&gt;is&lt;/code&gt; compares object references, not object equality&lt;/em&gt;&lt;/p&gt;</content><category term="Technical Solutions"/><category term="technical"/></entry><entry><title>My first experiences with a laser cutter</title><link href="https://andrewwegner.com/first-laser-cutter-experiences.html" rel="alternate"/><published>2025-01-02T10:15:00-06:00</published><updated>2025-01-02T10:15:00-06:00</updated><author><name>Andy Wegner</name></author><id>tag:andrewwegner.com,2025-01-02:/first-laser-cutter-experiences.html</id><summary type="html">&lt;p&gt;My local library recently received a Glowforge for their patrons to utilize. This article is about my experiences using the machine&lt;/p&gt;</summary><content type="html">
&lt;h2 id="introduction"&gt;Introduction&lt;a class="headerlink" href="#introduction" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;My local library spent a lot of time and money remodeling the last few years. Part of this remodel was adding an Innovation Lab with different tools that the public can utilize. These include a Cricut cutter, a Glowforge, a couple 3D printers, sewing machines, a T-shirt screen printer, and a few others I can't recall off hand. I am excited to try out several of these, but started with the laser cutter, because I have a home project that needs a few small things cut and engraved that I wasn't sure how I was going to do.&lt;/p&gt;
&lt;p&gt;The end goal is to get a set of two inch by one inch rectangles with various years engraved, scored or cut so that I can use these as labels. &lt;/p&gt;
&lt;h2 id="a-simple-start"&gt;A simple start&lt;a class="headerlink" href="#a-simple-start" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;After a bit of research, I found that what I wanted was relatively simple - at least compared to what the machine is able to do. To do more advanced things, I'd have to learn a lot more about how some software tools operate. For my project though, I used &lt;a href="https://inkscape.org/"&gt;Inkscape&lt;/a&gt;. I started by setting up the rectangle I wanted to use, rounding the corners, and adding a year to engrave. This became my basic template, to ensure that everything was the same size.&lt;/p&gt;
&lt;p&gt;&lt;img alt='A basic rounded rectangle with "2009" engraved' src="https://andrewwegner.com/images/laser_cutter/2009_cut_engrave.png"/&gt;&lt;/p&gt;
&lt;p&gt;In all of my SVG files, I only used three colors: &lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Red for cuts&lt;/li&gt;
&lt;li&gt;Black for engrave&lt;/li&gt;
&lt;li&gt;Blue for scoring&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;While I read the Glowforge can handle different colors, I noticed that other tooling and software could not and used these three colors. If I ever get access to another laser cutter - or buy one of my own - I don't want to have to redo all of my work.&lt;/p&gt;
&lt;p&gt;The important thing at this step was to select the text, then Text-&amp;gt;Object to path. Without this step, the Glowforge application didn't see the text, just the rectangle. &lt;/p&gt;
&lt;h2 id="getting-fancy"&gt;Getting Fancy&lt;a class="headerlink" href="#getting-fancy" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Once I had a basic cut done on basswood proof grade material from Glowforge, I wanted to try a few other designs. All of my examples below are using 2009, because that's the set I grabbed when taking pictures.&lt;/p&gt;
&lt;p&gt;&lt;img alt='A basic rounded rectangle with background engraved so "2009" pops out' src="https://andrewwegner.com/images/laser_cutter/2009_background_engraved.jpg"/&gt;&lt;/p&gt;
&lt;p&gt;With this, I engraved the background, instead of the text, so that the year would pop out. I didn't end up liking this one very much, because it makes the material so much thinner. This is obvious when I had multiple tiles of years that had not all been engraved this way. The thinness made these specific tiles feel flimsy. Since I didn't want to use this pattern for all years for the project, it wasn't going to work for only a couple.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Black acrylic with an outer border" src="https://andrewwegner.com/images/laser_cutter/2009_black_unmasked.jpg"/&gt;&lt;/p&gt;
&lt;p&gt;I tried a couple acrylic colors - Black and Blue - to see how it'd look. In this test, I wanted to deal with the thinness I mentioned above too. I did this by adding a small border around the outside, so that when the tiles were side by side, the outer edges were all the same thickness. I liked how this looked and would end up using a portion of this in some final designs. The black acrylic turned out well. &lt;/p&gt;
&lt;p&gt;&lt;img alt="Blue acrylic with an outer border, still with masking tape" src="https://andrewwegner.com/images/laser_cutter/2009_blue_masked.jpg"/&gt;&lt;/p&gt;
&lt;p&gt;Unfortunately, I didn't like the blue acrylic. This is how blue looks with the masking tape still in place. This tape is to prevent scorching marks. It looks ok with the tape in place, but once removed, the blue text just kind of got lost.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Blue acrylic without an outer border, with no masking tape" src="https://andrewwegner.com/images/laser_cutter/2009_blue_unmasked.jpg"/&gt;&lt;/p&gt;
&lt;p&gt;This test didn't have the outer border, but I don't think that would have helped me like it any better. The blue text just gets lost from any angle other than straight on. &lt;/p&gt;
&lt;p&gt;&lt;img alt="Individual numeral cut outs" src="https://andrewwegner.com/images/laser_cutter/2009_individual.jpg"/&gt;&lt;/p&gt;
&lt;p&gt;I cut out individual numbers as well. These look really good. Unfortunately, I didn't plan beyond the cut out, and dealing with four individual numerals on each item I wanted to label very quickly became a ton of work to ensure they were unmasked, aligned, and glued into place. They also didn't end up looking as good as the tiled items once I had them in place. &lt;/p&gt;
&lt;p&gt;Something I should have learned from this experiment that I was vaguely aware of, but didn't pay attention to because I was focused on the numbers themselves, was that the inner part of the numbers would come out. This was more important in the next two experiments.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Stacking acrylic on top of a wooden background" src="https://andrewwegner.com/images/laser_cutter/2009_stacked.jpg"/&gt;&lt;/p&gt;
&lt;p&gt;At this point I had decided that I liked how the black acrylic looked, but wanted to highlight a few years. I attempted one last test to see if I could utilize the narrower engraved look and stack it on top of another. The original goal had been to end up with the same thickness as the other tiles, but that didn't work as planned. &lt;/p&gt;
&lt;p&gt;That said, for the few years that I needed to highlight, this worked very well. To build this, I kept the lower layer - the wood - the same size as other tiles. I engraved like I had done for the acrylic tests above. Then on the acrylic itself, I made it the size of the inner border and reversed the numerals. I did this because I originally tested by engraving the acrylic down to size too, but it didn't look good. Instead, I kept the acrylic the original width and just cut out the numbers.&lt;/p&gt;
&lt;p&gt;Once stacked and glued together, I realized that I hadn't kept the inner pieces of the zeros. It is easy enough to go cut out a couple more numbers at the library, but at the same time, not bothering me enough to do so. &lt;/p&gt;
&lt;h2 id="conclusion"&gt;Conclusion&lt;a class="headerlink" href="#conclusion" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Over the holidays, I completed the project and got everything labeled. I'm pleased with how it turned out. I was not surprised at how easy the Glowforge itself is to use. Learning how to design the SVG cut files took longer than I expected. There are likely other software tools that could do the job better, but for this small project and for some experiments, Inkscape worked just fine.&lt;/p&gt;
&lt;p&gt;For my next project, I'd like to finish off the &lt;a href="https://andrewwegner.com/control-power-wled-relay.html"&gt;WLED&lt;/a&gt; work I did last summer. The original project didn't work as expected, but the WLED portion worked wonderfully. I could build a couple stand lights and laser cut out the base. I'd have to figure out how to model the aluminium rail to cut it correctly, but that sounds like a fun task.&lt;/p&gt;</content><category term="Side Activities"/><category term="technical"/><category term="meta"/></entry><entry><title>Moving MS Authenticator to a new phone without a personal account</title><link href="https://andrewwegner.com/ms-authenticator-without-personal-account.html" rel="alternate"/><published>2024-11-27T12:15:00-06:00</published><updated>2024-11-27T12:15:00-06:00</updated><author><name>Andy Wegner</name></author><id>tag:andrewwegner.com,2024-11-27:/ms-authenticator-without-personal-account.html</id><summary type="html">&lt;p&gt;The Microsoft Authenticator application allows you to transfer tokens to a new device if you have a personal Microsoft account. How do you do it if you don't have a personal account? This is a quick walk through of what worked for me.&lt;/p&gt;</summary><content type="html">
&lt;h2 id="introduction"&gt;Introduction&lt;a class="headerlink" href="#introduction" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Work utilizes the Microsoft Authenticator app as their 2FA application of choice. It's not my favorite, but it does what it needs to do, and since I only use the application for work things, it's nice to keep work accounts out of personal accounts in my other 2FA application. Recently, I got a new phone, and needed to migrate everything over to that device. As an aside, Android continues to make this easier and easier. &lt;/p&gt;
&lt;p&gt;Standard advice on how to migrate your token to a new device is to enable cloud backup, sign in to the new device and import the backup. The problem is when you go to enable Cloud Backup from the settings, you are hit with this message:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"You need a personal Microsoft account to use cloud backup"&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Well, I don't have a personal Microsoft account. &lt;/p&gt;
&lt;p&gt;&lt;em&gt;Aside: I can't provide screenshots of the Android application for this post because the application prevents screenshots from being taken. I appreciate this feature as part of the application for security purposes, but it's annoying when trying to write about it. Oh well...&lt;/em&gt;&lt;/p&gt;
&lt;h2 id="alternative-to-cloud-backup"&gt;Alternative to Cloud Backup&lt;a class="headerlink" href="#alternative-to-cloud-backup" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;I have set up MFA on my work account - obviously - and can access settings through the &lt;a href="https://aka.ms/mfasetup"&gt;Microsoft MFA setup page&lt;/a&gt;. From here, you'll be on the Security Info page. It will show the current authenticator device being utilized. You'll need the old device during this process to authenticate one last time.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Microsoft security information page showing my current MS Authenticator device" src="https://andrewwegner.com/images/ms_authenticator/security_info.png"/&gt;&lt;/p&gt;
&lt;p&gt;Click on "Add sign-in method" and then "Microsoft Authenticator"&lt;/p&gt;
&lt;p&gt;&lt;img alt="Options to pick from for adding a new signin method. For this, select Microsoft Authenticator" src="https://andrewwegner.com/images/ms_authenticator/new_method.png"/&gt;&lt;/p&gt;
&lt;p&gt;On your phone, ensure you have the MS Authenticator application installed. Then follow the on screen prompts between your computer and your new phone. You'll select "Work or School" and then scan the QR code that is common with 2FA/MFA applications.&lt;/p&gt;
&lt;p&gt;The last step in the process will be to send an authentication request to the new device. Do so, enter the verification code, and your new device will be added.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Device is approved after entering your verification code" src="https://andrewwegner.com/images/ms_authenticator/approved_device.png"/&gt;&lt;/p&gt;
&lt;p&gt;I recommend you wrap this up by removing your old device from the Security Info section now that the new device is approved. &lt;/p&gt;
&lt;h2 id="conclusion"&gt;Conclusion&lt;a class="headerlink" href="#conclusion" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The Microsoft authenticator application doesn't make moving to a new device as easy as some of its competitors. The requirement for a personal Microsoft account to go through the most commonly recommended way is also a non-starter if you don't have such an account already. Fortunately, the alternative posted above ended up working just fine. I assume I'll need a new phone again, and this should help at least me, remember that there is an easy enough way to migrate.&lt;/p&gt;</content><category term="Technical Solutions"/><category term="technical"/></entry><entry><title>Format USB drive with EFI partition in Windows</title><link href="https://andrewwegner.com/format-efi-usb-windows.html" rel="alternate"/><published>2024-11-13T13:00:00-06:00</published><updated>2024-11-13T13:00:00-06:00</updated><author><name>Andy Wegner</name></author><id>tag:andrewwegner.com,2024-11-13:/format-efi-usb-windows.html</id><summary type="html">&lt;p&gt;Windows protects EFI partitions from being deleted. This article is a quick walkthrough on how to format a USB drive with an EFI partition within Windows.&lt;/p&gt;</summary><content type="html">
&lt;h2 id="introduction"&gt;Introduction&lt;a class="headerlink" href="#introduction" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;I seem to collect USB drives. They are handed out at conferences. They are used as recovery media for new machines. They just randomly appear on my desk. I use them to build live Linux CDs, transferring media from one device to another, building recovery media, and generally the things you'd expect a USB drive to be used for. Generally, when I use a USB drive the first thing I do is format it. My USB drives are not for long term storage and I consider anything on them used one time then the drive goes back to the bin for reuse another time. &lt;/p&gt;
&lt;p&gt;An annoyance with this, though, is that when I use a drive to build a bootable backup media, often times an EFI partition will be created. As soon as this is created, Windows prevents that partition from being deleted in the future without taking additional steps. I have to go look those up every time because I do it infrequently enough to remember. This is a simple walkthrough of my process to solve this problem&lt;/p&gt;
&lt;p&gt;A USB drive with an EFI partition looks like this within the Disk Management tool in Windows&lt;/p&gt;
&lt;p&gt;&lt;img alt="A USB drive with an EFI partition and an unallocated partition" src="https://andrewwegner.com/images/efi-partition.png"/&gt;&lt;/p&gt;
&lt;h2 id="format-an-efi-partition"&gt;Format an EFI Partition&lt;a class="headerlink" href="#format-an-efi-partition" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;To remove this partition, utilize the built in &lt;code&gt;diskpart&lt;/code&gt; tool. &lt;code&gt;Win&lt;/code&gt; + &lt;code&gt;R&lt;/code&gt; and type in &lt;code&gt;diskpart&lt;/code&gt; and allow the application to run. &lt;/p&gt;
&lt;p&gt;First you need to find which drive you want to adjust partitions on. &lt;/p&gt;
&lt;div class="admonition attention"&gt;
&lt;p class="admonition-title"&gt;Attention&lt;/p&gt;
&lt;p&gt;Ensure you select the correct disk. Failure to do so could result in an inoperable system&lt;/p&gt;
&lt;/div&gt;
&lt;p&gt;Plug the USB drive into the machine and run &lt;code&gt;list disk&lt;/code&gt; to see all available disks.&lt;/p&gt;
&lt;div class="codehilight code"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;DISKPART&amp;gt; list disk

Disk ###  Status         Size     Free     Dyn  Gpt
--------  -------------  -------  -------  ---  ---
Disk 0    Online         3726 GB  1024 KB        &lt;span class="gs"&gt;*&lt;/span&gt;
&lt;span class="gs"&gt;Disk 1    Online          931 GB  1024 KB        *&lt;/span&gt;
Disk 2    Online           14 GB    14 GB
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;You can see that I have 3 drives detected. &lt;code&gt;Disk 2&lt;/code&gt; is my USB drive.&lt;/p&gt;
&lt;p&gt;Select the disk.&lt;/p&gt;
&lt;div class="codehilight code"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;DISKPART&amp;gt; sel disk 2

Disk 2 is now the selected disk.
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Then show the partitions on the disk.&lt;/p&gt;
&lt;div class="codehilight code"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;DISKPART&amp;gt; list partition

Partition ###  Type              Size     Offset
-------------  ----------------  -------  -------
Partition 1    System            4000 KB   850 KB
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;There is a single partition - the EFI partition. The rest of the drive is unallocated, as the screenshot above shows. Select this partition. If you have multiple partitions listed, you'll need to determine which is the EFI partition. In my case, since I'm going to be formatting the entire drive anyway, if there were multiple listed I'd select and delete each individual partition. &lt;/p&gt;
&lt;div class="codehilight code"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;DISKPART&amp;gt; sel partition 1

Partition 1 is now the selected partition.
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class="admonition attention"&gt;
&lt;p class="admonition-title"&gt;Attention&lt;/p&gt;
&lt;p&gt;These commands will delete a partition. If you haven't selected the right drive or partition things will break.&lt;/p&gt;
&lt;/div&gt;
&lt;p&gt;Finally, delete the EFI partition&lt;/p&gt;
&lt;div class="codehilight code"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;DISKPART&amp;gt; delete partition override
DiskPart successfully deleted the selected partition.
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;From the disk management tool, you can verify it's gone and format as normal.&lt;/p&gt;
&lt;p&gt;&lt;img alt="All EFI partitions removed" src="https://andrewwegner.com/images/efi-partition-removed.png"/&gt;&lt;/p&gt;</content><category term="Technical Solutions"/><category term="technical"/></entry><entry><title>Controlling power to LED lights with WLED Controller and a Relay switch</title><link href="https://andrewwegner.com/control-power-wled-relay.html" rel="alternate"/><published>2024-08-16T12:00:00-05:00</published><updated>2024-08-16T12:00:00-05:00</updated><author><name>Andy Wegner</name></author><id>tag:andrewwegner.com,2024-08-16:/control-power-wled-relay.html</id><summary type="html">&lt;p&gt;Wiring a lot of LEDs requires more power than the small WLED controller can handle, but leaving a large power supply running even when the lights are off is inefficient. This post talks about the progress on the project and how I wired in a relay to keep everything running efficiently.&lt;/p&gt;</summary><content type="html">
&lt;h2 id="introduction"&gt;Introduction&lt;a class="headerlink" href="#introduction" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;It's time to continue my summer project of setting up some outdoor LED lights. &lt;a href="https://andrewwegner.com/update-wled-ericsity-controller-0141.html"&gt;Previously&lt;/a&gt;, I &lt;a href="https://www.amazon.com/Ericsity-Controller-Addressable-WS2812B-SK6812/dp/B0CNVXY8NX"&gt;set up and updated the Ericsity controller&lt;/a&gt; with &lt;a href="https://kno.wled.ge/"&gt;WLED&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I am putting up approximately 20 meters - 65 feet - of LEDs. Total this will be nearly 2,000 individual LEDs and approximately 665 individually controlled LED segments (3 LEDs per segment). This will take more power than the little controller can handle. To demonstrate the problem of powering all of these LEDs with only the controller, look at this image:&lt;/p&gt;
&lt;p&gt;&lt;img alt="Power drop across 20 meters of LEDs" src="https://andrewwegner.com/images/wled/voltage-drop.png"/&gt;&lt;/p&gt;
&lt;p&gt;These strips are wired with the end of one strip connected to the start of the next. The top strip is connected directly to the controller. The one below is the end of strip two, followed by the start of strip three and the bottom is the very end of the full run of LEDs. The lights are all set to the same color, but as you can see they clearly aren't the same color. The voltage drop across 65 feet of LEDs means that the LEDs are the end can't get enough power to match their earlier siblings.&lt;/p&gt;
&lt;h2 id="power-injection"&gt;Power Injection&lt;a class="headerlink" href="#power-injection" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The solution to this specific problem is to inject power into the LED strips. I picked up some &lt;a href="https://www.amazon.com/dp/B0957T1S9C"&gt;WAGO connectors&lt;/a&gt; and a &lt;a href="https://www.amazon.com/dp/B0BXTP524R"&gt;NUOFUWEI power supply&lt;/a&gt;. I also had some 18 gauge wire on hand. With this power unit, I can easily set up three injection points.&lt;/p&gt;
&lt;p&gt;The goal was to inject power at the start of strip 1, right where the controller is connecting in the image above. Then inject between strips 2 and 3, in the middle of the run. Finally, I injected power at the end of strip 4. With these three, equally spaced injection points, I was able to get a nice uniform color across the entire 20 meter run.&lt;/p&gt;
&lt;p&gt;&lt;img alt="LED strip with power injection shows uniform coloring" src="https://andrewwegner.com/images/wled/equal-voltage.png"/&gt;&lt;/p&gt;
&lt;p&gt;Problem Solved! Right?&lt;/p&gt;
&lt;p&gt;Not exactly.&lt;/p&gt;
&lt;h2 id="turning-off-the-psu"&gt;Turning off the PSU&lt;a class="headerlink" href="#turning-off-the-psu" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;WLED controls the lights, but doesn't control the power supply with this wiring setup. Turning off the lights within WLED does turn off the LEDs, but the power supply continues to run. It's not drawing at full load, but it is drawing power and the cooling fan is active. It's noticeable and unneeded. I want a way to turn off the power supply AND the LEDs at the same time. &lt;/p&gt;
&lt;p&gt;I can't power the WLED controller from the large power supply to do this, because if I turn off the power supply that'd also turn off the WLED controller. I'll need to power the WLED controller independently from the lights. Fortunately, this won't be a problem. &lt;/p&gt;
&lt;p&gt;The next step is figuring out how I can use WLED to control the larger power supply. Fortunately, &lt;a href="https://kno.wled.ge/features/relay-control/"&gt;WLED has the ability to control a relay&lt;/a&gt;, which I can use to control the power supply. The Ericsity controller also has two output data pins. While I don't think the second one was built in to control a relay, it works perfectly here. &lt;/p&gt;
&lt;h3 id="wiring"&gt;Wiring&lt;a class="headerlink" href="#wiring" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;I purchased a &lt;a href="https://capitaloneshopping.com/p/hi-letgo-5-v-1-channel-relay-mod/2RDBGLR8VL"&gt;HiLetGo relay&lt;/a&gt; so that I could toggle the larger power supply on and off. To do this, it's important that the data line and the ground are common among the controller, the power supply and the LEDs.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Wiring diagram for WLED controller, external power supply, LEDs and a relay controlling the external PSU" src="https://andrewwegner.com/images/wled/deck-lights-wled.png"/&gt;&lt;/p&gt;
&lt;p&gt;The diagram above is a rough schematic of how I wired this. The controller sends data, but not power, to the LEDs. Data was on GPIO 16. The important part here is that the data line is shared across all of the strips. I did not need a signal booster for my project, and because I'm about to use the second exposed data channel for the relay, I had to ensure that this single channel could send a signal down the entire length of the strip. Fortunately, I didn't have any issues.&lt;/p&gt;
&lt;p&gt;The wiring ground was tied into the PSU and the LEDs as well. The common ground is important.&lt;/p&gt;
&lt;p&gt;On the other side of the diagram is the relay. I put this relay between the wall and the PSU. When WLED sent an &lt;code&gt;ON&lt;/code&gt; signal, it would close the relay, turning on the PSU and the LEDs. GPIO 2 was tied to the relay and within &lt;code&gt;LED Preferences&lt;/code&gt;, the Relay Pin was set to GPIO 2. &lt;/p&gt;
&lt;p&gt;One quick power cycle from within WLED is required at this point. Press the power button in the UI to turn everything off, press it once more to turn it on. As long as the PSU is plugged into the wall, you should hear it fire up and see the LEDs turn on. If you press the power button again, the LEDs turn off, the relay clicks, shuts down the PSU and only the WLED controller remains active.&lt;/p&gt;
&lt;h2 id="success"&gt;Success&lt;a class="headerlink" href="#success" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;With the relay in place, the LEDs don't draw phantom power while off because the PSU isn't active. The added benefit, at least for this specific power unit, is that the fans aren't running constantly so it's not as loud. While this will eventually be outside, I'd still prefer to not hear the fan when the lights are off. While they are active it's not going to be bothering me, because I'll likely have music playing for the sound reactive features which will easily be louder than this fan.&lt;/p&gt;
&lt;p&gt;The next step in the project is going to be to get this set up outside. &lt;/p&gt;</content><category term="Technical Solutions"/><category term="technical"/><category term="wled"/></entry><entry><title>Setting up and updating the Ericsity WLED Controller from 0.13.3 to 0.14.1</title><link href="https://andrewwegner.com/update-wled-ericsity-controller-0141.html" rel="alternate"/><published>2024-06-20T15:45:00-05:00</published><updated>2024-06-20T15:45:00-05:00</updated><author><name>Andy Wegner</name></author><id>tag:andrewwegner.com,2024-06-20:/update-wled-ericsity-controller-0141.html</id><summary type="html">&lt;p&gt;The Ericsity WLED controller comes with WLED 0.13.3 preinstalled and only offers the ability to update to 0.13.4. This walks through setting up the controller for the first time and moving to 0.14.1 while maintaining the sound reactive features the controller advertises.&lt;/p&gt;</summary><content type="html">
&lt;h2 id="introduction"&gt;Introduction&lt;a class="headerlink" href="#introduction" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;My summer time project this year is to set up some LED lights outside. After testing out a couple Govee strips that I didn't like because I couldn't diffuse the individual lights away, I finally found an LED strip I liked. The next step was to control these strips so that I could do more than solid colors or the default rainbow every LED strip has. I settled on using the amazing &lt;a href="https://kno.wled.ge/"&gt;WLED project&lt;/a&gt; to control the lights. To speed up the project, I decided to get a prebuilt controller and eventually selected the &lt;a href="https://www.amazon.com/Ericsity-Controller-Addressable-WS2812B-SK6812/dp/B0CNVXY8NX"&gt;Ericsity controller with a built in mic&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Side note: This post is just about how I updated the controller. The next article in the post is available, and talks about how &lt;a href="https://andrewwegner.com/control-power-wled-relay.html"&gt;I am controlling a larger power supply with the WLED controller&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="the-problem"&gt;The Problem&lt;a class="headerlink" href="#the-problem" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The controller arrived and was easy to set up. I wired up my strips in the basement to ensure I didn't have any power problems and then determined where I needed to make cuts and solder points once this was outside. Then I went to set up &lt;a href="https://kno.wled.ge/features/segments/"&gt;segments&lt;/a&gt; in WLED so that I could control effects in specific areas.&lt;/p&gt;
&lt;p&gt;The problem I quickly encountered was that I am outlining my deck with these lights and right in the middle of the deck is a set of steps that I also wanted to light up and then the rest of the deck rail. WLED doesn't have the ability (at least that I've found) to span an effect across multiple segments and I'd need multiple segments for this. &lt;/p&gt;
&lt;p&gt;At minimum I'd need 3 segments - The rail, the steps, the rest of the rail. If I want an effect to span the entire rail, I can't do that with the steps in the middle. Further research pointed me to the ability to &lt;a href="https://kno.wled.ge/advanced/mapping/"&gt;remap the physical LED order&lt;/a&gt; to a logical LED order. After trying to set this up, it wasn't feature complete in 0.13, but did appear to be in 0.14.&lt;/p&gt;
&lt;p&gt;However, the Ericsity - rightfully so - utilizes a &lt;a href="https://github.com/atuline/WLED"&gt;stable version of WLED&lt;/a&gt; that only allows you to &lt;a href="https://github.com/atuline/WLED/releases"&gt;update to 0.13.4&lt;/a&gt; and the README has the following note:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;This repository is still maintained, and will receive bugfixes. However no new features will be added.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img alt="WLED Sound Reactive update page showing that upgrades are only available through version 0.13.4" src="https://andrewwegner.com/images/wled/wled-update-page.png"/&gt;&lt;/p&gt;
&lt;h2 id="setting-up-ericsity-controller"&gt;Setting up Ericsity Controller&lt;a class="headerlink" href="#setting-up-ericsity-controller" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Let's take a step back and get the controller set up and then focus on upgrading it. Fortunately, Ericsity makes this pretty easy.&lt;/p&gt;
&lt;p&gt;Step 1 is to plug in the controller. After a few seconds, you will see a new network available on your phone called &lt;code&gt;WLED-AP&lt;/code&gt;. Join that network. From your browser navigate to &lt;code&gt;4.3.2.1&lt;/code&gt; and select "WIFI SETTINGS". &lt;/p&gt;
&lt;p&gt;&lt;img alt="WLED install page" src="https://andrewwegner.com/images/wled/install1.png"/&gt;&lt;/p&gt;
&lt;p&gt;The goal is to add this controller to your wireless network. Do this by adding your network name and password into the first two text boxes. I also updated the mDNS setting, but that's optional. Then save and connect.&lt;/p&gt;
&lt;p&gt;&lt;img alt="WLED network set up page" src="https://andrewwegner.com/images/wled/install2.png"/&gt;&lt;/p&gt;
&lt;p&gt;Finally, install the WLED mobile application if you are going to manage this via your phone. On an android device you can find it in the Google Play store with the name &lt;a href="https://play.google.com/store/apps/details?id=com.aircoookie.WLED&amp;amp;hl=en_US"&gt;WLED&lt;/a&gt;. Reconnect to your wireless network and the one you just added the controller to. Once installed and reconnected to the network, open the application. &lt;/p&gt;
&lt;p&gt;If you have already installed and configured a controller, you'll see your previous controllers listed. To add the new controller, click on the &lt;code&gt;+&lt;/code&gt; sign in the upper right. Then "Start Discovery". When the controller is found, you can press the check button.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Searching for and adding a new controller to the WLED application" src="https://andrewwegner.com/images/wled/install3.png"/&gt;&lt;/p&gt;
&lt;p&gt;The new controller will be listed on the main WLED page with the default name of &lt;code&gt;WLED-SoundReactive&lt;/code&gt;. Select that controller by tapping it. Then select "Config" along the top and "Security &amp;amp; Updates" at the bottom.&lt;/p&gt;
&lt;p&gt;Scroll all the way down to the bottom of this page and you should see that you have the following version installed:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;WLED SR version 0.13.3&lt;/code&gt;&lt;/p&gt;
&lt;h2 id="update-to-014x"&gt;Update to 0.14.x&lt;a class="headerlink" href="#update-to-014x" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;From the Security &amp;amp; Updates page, it makes sense to try and perform an update. If you click "Manual OTA Update" you'll notice that only version &lt;code&gt;0.13.3&lt;/code&gt; is available via this link though. It's time to do a manual update!&lt;/p&gt;
&lt;p&gt;I am utilizing the &lt;a href="https://github.com/MoonModules/WLED"&gt;MoonModules branch&lt;/a&gt; because the README for the default install says that changes should be made against this branch.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Pull Requests should be created against the MoonModules mdev branch.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I performed this OTA upgrade from my computer by navigating to the mDNS I set up previously. It can also be accessed by the IP address of the controller in your browser.&lt;/p&gt;
&lt;p&gt;At the time of this post, the &lt;a href="https://github.com/MoonModules/WLED/releases"&gt;current release&lt;/a&gt; was &lt;code&gt;0.14.1-b30&lt;/code&gt;. This was released approximately 6 months ago. I briefly skimmed through recent issues and pull requests to see if I should find a more recent build. There was a &lt;a href="https://github.com/MoonModules/WLED/issues/130"&gt;crash issue&lt;/a&gt; reported with the &lt;code&gt;ripple&lt;/code&gt; effects. Since I like that particular effect, I decided to go with a newer build than the official release. &lt;/p&gt;
&lt;h3 id="official-build"&gt;Official build&lt;a class="headerlink" href="#official-build" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;If you are going to stick with an official release, navigate to the &lt;a href="https://github.com/MoonModules/WLED/releases"&gt;release page&lt;/a&gt; and download the binary file you want to install. For the Ericsity, I found that the generic ESP32 build worked. If you want to use this, look for the file titled &lt;code&gt;WLEDMM_0.14.1-b30.36_esp32_4MB_M.bin&lt;/code&gt;. You could also use the &lt;code&gt;WLEDMM_0.14.1-b30.36_esp32_4MB_S.bin&lt;/code&gt;. Download this file.&lt;/p&gt;
&lt;h3 id="selecting-a-more-recent-build"&gt;Selecting a more recent build&lt;a class="headerlink" href="#selecting-a-more-recent-build" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Since I wanted a fix for the item I &lt;a href="https://github.com/MoonModules/WLED/issues/130"&gt;found&lt;/a&gt;, I opted to download a recent build of the &lt;code&gt;mdev&lt;/code&gt; branch. This can be accomplished by navigating to the build pipeline and filtering for the &lt;code&gt;mdev&lt;/code&gt; branch. This is on Github and available via this direct link to the &lt;a href="https://github.com/MoonModules/WLED/actions/workflows/wled-ci.yml?query=branch%3Amdev"&gt;mdev pipeline&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Recent builds on Github. Look for mdev branch" src="https://andrewwegner.com/images/wled/github-builds.png"/&gt;&lt;/p&gt;
&lt;p&gt;From here, select the build you want and scroll all the way to the bottom of the page. Find the binary you want to download. In my case, I wanted the &lt;code&gt;firmware-esp32_4MB_M&lt;/code&gt; and download it. Once downloaded, extract it so that you can upload the &lt;code&gt;.bin&lt;/code&gt; file within the WLED page. &lt;/p&gt;
&lt;p&gt;&lt;img alt="Select the build to utilize. For the Ericsity controller, I found the firmware-esp32_4MB_M version works" src="https://andrewwegner.com/images/wled/github-build-version.png"/&gt;&lt;/p&gt;
&lt;h3 id="press-the-button"&gt;Press the button!&lt;a class="headerlink" href="#press-the-button" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;In WLED's Security and Update page, select this binary and then click "Update!"&lt;/p&gt;
&lt;p&gt;&lt;img alt="Upload the binary to utilize as the new image" src="https://andrewwegner.com/images/wled/update1.png"/&gt;&lt;/p&gt;
&lt;p&gt;The update took less than a minute for me. While the new image was being installed, I was told not to close or refresh the page.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Update installing, don't close or refresh" src="https://andrewwegner.com/images/wled/update2.png"/&gt;&lt;/p&gt;
&lt;p&gt;When the update is complete, the controller will reboot. &lt;/p&gt;
&lt;p&gt;&lt;img alt="Update complete, rebooting" src="https://andrewwegner.com/images/wled/update3.png"/&gt;&lt;/p&gt;
&lt;p&gt;To confirm that everything has been updated, click on "Config". Immediately, you'll notice a lot more options. Scroll to the bottom and select "Security &amp;amp; Updates" and at the bottom you'll have an About section that lists the version you selected to install. Mine looks like this:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;WLEDMM version 0.14.1-b31.38 ☾&lt;/code&gt;&lt;/p&gt;
&lt;h3 id="fixing-sound-reactive"&gt;Fixing Sound Reactive&lt;a class="headerlink" href="#fixing-sound-reactive" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;During testing, I noticed that the sound reaction wasn't working. I found the AudioReactive plugin was disabled by default. This is an easy fix. In the WLED application (or web page), click on "Info" then click the power button icon next to "AudioReactive". However, this isn't enough to solve the problem. Click on "Config" then scroll down to AudioReactive. First make sure it is enabled (it should be after the power button selection above). &lt;/p&gt;
&lt;p&gt;The digitalmic section needs to be modified. The Ericsity controller has the microphone on the following pins:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Microphone Type: Generic I2S&lt;/li&gt;
&lt;li&gt;Pin I2S SD: 26&lt;/li&gt;
&lt;li&gt;Pin I2S WS: 5&lt;/li&gt;
&lt;li&gt;Pin I2S SCK: 21&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img alt="Correct audio settings for Ericsity controller. Pin 26 for SD, Pin 5 for WS and Pin 21 for SCK" src="https://andrewwegner.com/images/wled/correct-audio.png"/&gt; &lt;/p&gt;
&lt;p&gt;I found that pin 5 was selected for another plugin. If you find this too, navigate back to Config page and select "Rotary-Encoder". I don't utilize this plugin, but it is part of the &lt;code&gt;M&lt;/code&gt; build I downloaded. Disable the plugin and change the &lt;code&gt;CLK Pin&lt;/code&gt; to be &lt;code&gt;undefined&lt;/code&gt;. Then save this change and go back to the AudioReactive plugin and set it up with the pin layout above.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Disable the Rotary-Encoder plugin and set the CLK pin to undefined" src="https://andrewwegner.com/images/wled/disabled-plugin.png"/&gt;&lt;/p&gt;
&lt;p&gt;Finally, reboot the controller by selecting "Info" and rebooting the controller at the bottom.&lt;/p&gt;
&lt;h2 id="good-to-go"&gt;Good to go&lt;a class="headerlink" href="#good-to-go" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Now that the controller has rebooted, audio reactions are working again. The branch I've updated my controller to is actively maintained for the 0.14 branch, even if it is several months behind the main line. I'm satisfied with this for now and will begin experimenting with the led mappings that I need that started this entire upgrade process.&lt;/p&gt;</content><category term="Technical Solutions"/><category term="technical"/><category term="wled"/></entry><entry><title>Python Gotcha: strip, lstrip, rstrip can remove more than expected</title><link href="https://andrewwegner.com/python-gotcha-strip-functions-unexpected-behavior.html" rel="alternate"/><published>2024-03-29T09:00:00-05:00</published><updated>2024-03-29T09:00:00-05:00</updated><author><name>Andy Wegner</name></author><id>tag:andrewwegner.com,2024-03-29:/python-gotcha-strip-functions-unexpected-behavior.html</id><summary type="html">&lt;p&gt;The Python strip, lstrip, and rstrip functions can have unexpected behavior. Even though this is documented, non-default values passed to these functions can lead to unexpected results and how Python 3.9 solved this with two new functions.&lt;/p&gt;</summary><content type="html">
&lt;h2 id="introduction"&gt;Introduction&lt;a class="headerlink" href="#introduction" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;As a software engineer, you've cleaned your fair share of dirty strings. Removing leading or trailing spaces is probably one of the most common things done to user input. &lt;/p&gt;
&lt;p&gt;In Python, this is done with the &lt;a href="https://docs.python.org/3.10/library/stdtypes.html#str.strip"&gt;&lt;code&gt;.strip()&lt;/code&gt;&lt;/a&gt;, &lt;a href="https://docs.python.org/3.10/library/stdtypes.html#str.lstrip"&gt;&lt;code&gt;.lstrip()&lt;/code&gt;&lt;/a&gt; or &lt;a href="https://docs.python.org/3.10/library/stdtypes.html#str.rstrip"&gt;&lt;code&gt;.rstrip()&lt;/code&gt;&lt;/a&gt; functions and generally looks like this:&lt;/p&gt;
&lt;div class="codehilight code"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&amp;gt;&amp;gt;&amp;gt; "     Andrew Wegner     ".lower().strip()
'andrew wegner'
&amp;gt;&amp;gt;&amp;gt; "     Andrew Wegner     ".lower().lstrip()
'andrew wegner     '
&amp;gt;&amp;gt;&amp;gt; "     Andrew Wegner     ".lower().rstrip()
'     andrew wegner'
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;That's pretty straightforward and nothing unexpected is going on. &lt;/p&gt;
&lt;h2 id="gotcha"&gt;Gotcha&lt;a class="headerlink" href="#gotcha" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The Gotcha is that each of these functions take a list of characters that can be removed. &lt;/p&gt;
&lt;div class="codehilight code"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&amp;gt;&amp;gt;&amp;gt; "Andrew Wegner".lower().rstrip(" wegner")
'and'
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;What happened? Why wasn't the result just&lt;/p&gt;
&lt;div class="codehilight code"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;'andrew'
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2 id="explanation"&gt;Explanation&lt;a class="headerlink" href="#explanation" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Read the line from the documentation again, carefully:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;A list of &lt;strong&gt;characters&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Not a list of strings.&lt;/p&gt;
&lt;p&gt;This is explicitly spelled out in the documentation, with an example, showing what the implications are. However, for a new developer, it's unexpected behavior. After all, these seem like intuitive functions. &lt;/p&gt;
&lt;p&gt;The example above does the following:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Receives a list of characters to remove. In this case it is all letters in my last name, plus the space character: &lt;code&gt;wegner&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Lower case all letters in the input string, resulting in &lt;code&gt;andrew wegner&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;From the right hand side of the string, begin removing characters that are in the input list. Stop when you encounter a character not in the list. In this case that means that &lt;code&gt;rengew wer&lt;/code&gt; are removed (right to left) and then the &lt;code&gt;d&lt;/code&gt; in &lt;code&gt;andrew&lt;/code&gt; is encountered so that &lt;code&gt;rstrip&lt;/code&gt; function stops. &lt;/li&gt;
&lt;li&gt;Return the remaining string of &lt;code&gt;and&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="solution"&gt;Solution&lt;a class="headerlink" href="#solution" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Python has two functions that will correctly remove a &lt;strong&gt;string&lt;/strong&gt; - &lt;a href="https://docs.python.org/3.10/library/stdtypes.html#str.removesuffix"&gt;&lt;code&gt;.removesuffix()&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://docs.python.org/3.10/library/stdtypes.html#str.removeprefix"&gt;&lt;code&gt;.removeprefix()&lt;/code&gt;&lt;/a&gt; for right and left side removals. &lt;/p&gt;
&lt;div class="codehilight code"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&amp;gt;&amp;gt;&amp;gt; "Andrew Wegner".lower().removesuffix(" wegner")
'andrew'
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;These two functions were introduced in Python 3.9 as part of &lt;a href="https://peps.python.org/pep-0616/"&gt;PEP-616&lt;/a&gt;. In the PEP, it explicitly calls out the confusion users have about the &lt;code&gt;*strip()&lt;/code&gt; functions and how they behave. These two were introduced to allow the desired behavior. &lt;/p&gt;
&lt;p&gt;One important note is that these two &lt;code&gt;remove*&lt;/code&gt; functions will only remove &lt;em&gt;at most&lt;/em&gt; one instance of the string.&lt;/p&gt;
&lt;div class="codehilight code"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&amp;gt;&amp;gt;&amp;gt; "Andrew Wegner Wegner".lower().removesuffix(" wegner")
'andrew wegner'
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;</content><category term="Technical Solutions"/><category term="technical"/></entry><entry><title>Installing the Mobile Security Framework (MobSF) on Windows using Docker</title><link href="https://andrewwegner.com/install-mobsf-on-windows-docker.html" rel="alternate"/><published>2024-02-19T15:00:00-06:00</published><updated>2024-02-19T15:00:00-06:00</updated><author><name>Andy Wegner</name></author><id>tag:andrewwegner.com,2024-02-19:/install-mobsf-on-windows-docker.html</id><summary type="html">&lt;p&gt;A short article on installing the Mobile Security Framework on Windows utilizing Docker and then using it to quickly analyze an APK.&lt;/p&gt;</summary><content type="html">
&lt;h2 id="introduction"&gt;Introduction&lt;a class="headerlink" href="#introduction" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;I recently needed to poke around an APK. I'd done this sort of thing before, but it's been years so the process of setting up an Android environment was unfamiliar. Instead of spending a day reimmersing myself for what was (hopefully) going to be a 10-15 minute thing, I turned to another tool: &lt;a href="https://github.com/MobSF/Mobile-Security-Framework-MobSF"&gt;Mobile Security Framework&lt;/a&gt; (also known as MobSF)&lt;/p&gt;
&lt;h2 id="setting-up-docker"&gt;Setting up Docker&lt;a class="headerlink" href="#setting-up-docker" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;There are countless blog posts, articles, and YouTube videos on how to set up the perfect docker environment. This was a quick task. I didn't need "perfect", I needed good enough. At the time, I was on a Windows 11 machine, so that's what I had to work with. The goal was to &lt;a href="https://docs.docker.com/get-docker/"&gt;get Docker&lt;/a&gt; operational as quickly as possible.&lt;/p&gt;
&lt;p&gt;Why? MobSF has a quick setup with two lines of Docker code. &lt;/p&gt;
&lt;p&gt;Using the &lt;a href="https://docs.docker.com/desktop/install/windows-install/"&gt;Install Docker for Windows&lt;/a&gt; documentation, I downloaded the installable and started the process. The installation took a couple minutes on the device I was on and required me to log out and back in. Total install time was less than 5 minutes.&lt;/p&gt;
&lt;h2 id="installing-mobsf"&gt;Installing MobSF&lt;a class="headerlink" href="#installing-mobsf" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Once logged back in, I fired up the command line. The &lt;a href="https://github.com/MobSF/Mobile-Security-Framework-MobSF"&gt;MobSF README&lt;/a&gt; provides two lines to execute on the command line to pull the latest image. I didn't go through the process of setting up a dynamic analyzer. I didn't think I'd need it (and as luck would have it in this case, I was correct).&lt;/p&gt;
&lt;p&gt;Run the two commands:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;docker pull opensecurity/mobile-security-framework-mobsf:latest
docker run -it --rm -p 8000:8000 opensecurity/mobile-security-framework-mobsf:latest&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;This will pull the latest version of MobSF and then run it on port 8000 on the local machine. Once it's running you can access it in your browser by going to &lt;code&gt;http://127.0.0.1:8000&lt;/code&gt;&lt;/p&gt;
&lt;h2 id="find-your-apk"&gt;Find your APK&lt;a class="headerlink" href="#find-your-apk" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Theoretically, you'll be running this against an APK you've built and developed. In that case, you'll have the APK handy and can load it into the UI. I, unfortunately, didn't have that luxury. Fortunately, getting the APK is relatively simple.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Go to the &lt;a href="https://play.google.com/store/apps"&gt;Google Playstore&lt;/a&gt; and search for the application&lt;/li&gt;
&lt;li&gt;Navigate into the details page of the application (you should see the option to install it)&lt;/li&gt;
&lt;li&gt;Copy the URL to this page&lt;/li&gt;
&lt;li&gt;Go to &lt;a href="https://apkcombo.com/downloader/"&gt;APKCombo&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Paste the URL you copied in step 3 into the appropriate text box and click "Generate Download Link"&lt;/li&gt;
&lt;li&gt;After a few seconds, click the download icon next to the link that was generated&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;You now have the APK to use.&lt;/p&gt;
&lt;h2 id="using-mobsf"&gt;Using MobSF&lt;a class="headerlink" href="#using-mobsf" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;With the APK in hand, and MobSF running, navigate to &lt;code&gt;http://127.0.0.1:8000&lt;/code&gt;. Upload the APK and wait a couple of minutes. If you are watching the logs, you'll see it performing multiple steps behind the scenes. Once it's done analyzing the APK, it'll drop you to the dashboard.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Analyzing an APK" src="https://andrewwegner.com/images/mobsf/analyze.png"/&gt;&lt;/p&gt;
&lt;p&gt;&lt;img alt="Analyzing an APK with logs" src="https://andrewwegner.com/images/mobsf/analyze-logs.png"/&gt;&lt;/p&gt;
&lt;p&gt;The dashboard has a &lt;em&gt;lot&lt;/em&gt; of detail about the APK. Scroll through to see the permissions it's utilizing, certificates attached to it, potential security vulnerabilities, code analysis, detected URLs in the code, hard coded secrets, and strings. All of these would help a team build a better and more secure mobile application. &lt;/p&gt;
&lt;h2 id="conclusion"&gt;Conclusion&lt;a class="headerlink" href="#conclusion" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;For my purpose, this 15 minute exercise not only answered the question I was seeking it also raised a few items that will need to be corrected before the next release. Introducing security practices with a tool like MobSF would be incredibly beneficial to a team tasked with building an Android application. Further, adding in the dynamic analysis capabilities can provide more details on areas to improve. &lt;/p&gt;
&lt;p&gt;While I didn't need those to answer my question, I'd encourage someone looking to introduce this to a team, to look at the &lt;a href="https://mobsf.github.io/docs/#/dynamic_analyzer"&gt;capabilities of MobSF&lt;/a&gt;&lt;/p&gt;</content><category term="Technical Solutions"/><category term="technical"/></entry><entry><title>Review of Data Analysis with Polars Udemy course</title><link href="https://andrewwegner.com/data-analysis-with-polars-review.html" rel="alternate"/><published>2024-01-29T11:30:00-06:00</published><updated>2024-01-29T11:30:00-06:00</updated><author><name>Andy Wegner</name></author><id>tag:andrewwegner.com,2024-01-29:/data-analysis-with-polars-review.html</id><summary type="html">&lt;p&gt;A review of the Udemy course: Data Analysis with Polars&lt;/p&gt;</summary><content type="html">
&lt;h2 id="introduction"&gt;Introduction&lt;a class="headerlink" href="#introduction" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;After the &lt;a href="https://andrewwegner.com/learn-analytics-with-polars-review.html"&gt;previous course about Polars&lt;/a&gt;, I was excited to learn more. I settled on &lt;a href="https://www.udemy.com/course/data-analysis-with-polars/"&gt;Data Analysis with Polars&lt;/a&gt; with Liam Brannigan instructing. I was drawn to this because of the mention of visualization using &lt;a href="https://matplotlib.org/"&gt;Matplotlib&lt;/a&gt;, &lt;a href="https://seaborn.pydata.org/"&gt;Seaborn&lt;/a&gt;, &lt;a href="https://altair-viz.github.io/"&gt;Altair&lt;/a&gt; and &lt;a href="https://plotly.com/python/getting-started/"&gt;Plotly&lt;/a&gt;. The course is billed at 2.5 hours long and is currently going for $85, though I did pick this up during one of Udemy's many sales. &lt;/p&gt;
&lt;p&gt;Let's get started on the review and why I rated it so low.&lt;/p&gt;
&lt;h2 id="course"&gt;Course&lt;a class="headerlink" href="#course" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;I have multiple complaints about this course. &lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The entire course is reading from pre-built notebooks or slides.&lt;/li&gt;
&lt;li&gt;The last 26 out of 27 lectures are one page slides. &lt;/li&gt;
&lt;li&gt;The quizzes in the middle of the courses are not effective, because the course is not designed for the learner to actually do any coding.&lt;/li&gt;
&lt;li&gt;The exercises at the end of lectures are never revisited. &lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This course felt like the instructor was given a bunch of Jupyter notebooks by a more senior instructor and told to go teach the class. There is a lot of reading directly from the notebook, quickly glossing over code, and then talking through the output of the code. Unlike the &lt;a href="https://andrewwegner.com/learn-analytics-with-polars-review.html"&gt;last course&lt;/a&gt;, this one doesn't even feel like a cookbook of usefulness. It's just a glorified slide deck.&lt;/p&gt;
&lt;p&gt;Let me show you what I mean. This is the course list's last 4 sections. I've collapsed them to show only the section header so it's a reasonable sized screenshot. &lt;/p&gt;
&lt;p&gt;&lt;img alt="Last 4 sections of the course - 27 lections should take 7 minutes only?" src="https://andrewwegner.com/images/polars-course-list-concerns.png"/&gt;&lt;/p&gt;
&lt;p&gt;There are 27 lectures in these 4 sections. The total estimated time for these 27 lectures - 7 minutes. There is only 1 video in this entire group of sections. The other 26 out of 27 lectures are one page slides. The slides are "What you'll learn by the end of this lecture" slides and a link to a Jupyter notebook. &lt;/p&gt;
&lt;p&gt;The quizzes scattered through out the course are also ineffective. Many of the questions asked are asking what code provided is correct. These can be helpful, if the learner has done any hands on work during the course. But, this course isn't designed that way. It's an instructor reading slides, hand waving at some code, and then executing the code to show it works. It's ineffective.&lt;/p&gt;
&lt;p&gt;The same problem holds true with the exercises at the end of many lectures. They are designed to get the student more experience and I started doing them early in the course. But, they are never revisited. The instructor doesn't talk about them. It's just homework that's assigned as busy work, essentially. &lt;/p&gt;
&lt;h2 id="conclusion"&gt;Conclusion&lt;a class="headerlink" href="#conclusion" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Avoid this course. Nearly half the course is just a single slide for the lectures. The items I was interested in learning about - visualizations - were among those one page slides. The course is designed like a glorified README, and its not worth the sales price I paid, let alone the full price. &lt;/p&gt;
&lt;p&gt;&lt;a href="https://www.udemy.com/certificate/UC-8315301e-a632-458f-8b3c-9392e076d2fa/"&gt;&lt;img alt="Data Analysis with Polars Completion Certificate" src="https://andrewwegner.com/images/udemy-data-analysis-polars.jpg"/&gt;&lt;/a&gt;&lt;/p&gt;</content><category term="Review"/><category term="review"/><category term="technical"/><category term="learning"/></entry><entry><title>Review of Learn Data Analytics with Polars (Python) Course</title><link href="https://andrewwegner.com/learn-analytics-with-polars-review.html" rel="alternate"/><published>2024-01-25T08:30:00-06:00</published><updated>2024-01-25T08:30:00-06:00</updated><author><name>Andy Wegner</name></author><id>tag:andrewwegner.com,2024-01-25:/learn-analytics-with-polars-review.html</id><summary type="html">&lt;p&gt;A review of the Udemy course: Learn Data Analytics with Polars (Python) in just 2 hours!&lt;/p&gt;</summary><content type="html">
&lt;h2 id="introduction"&gt;Introduction&lt;a class="headerlink" href="#introduction" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;I started this entire &lt;a href="https://andrewwegner.com/category/review.html"&gt;Course Reviews&lt;/a&gt; series 7 years ago, with a &lt;a href="https://andrewwegner.com/data-analysis-with-pandas-review.html"&gt;post about a Pandas course&lt;/a&gt;. It went so well, I continued both taking various courses to expand my own knowledge and sharing my experiences with those courses here. It seems fitting to look at a Pandas competitors: &lt;a href="https://pola.rs/"&gt;Polars&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I've used Pandas for years and it's done the job well enough. But, Polars is gaining traction in the industry and one of the data engineers at work mentioned it as a possible tool to look at. I like to be informed when my engineers make tooling recommendations, so I took it upon myself to learn a little more about Polars. I did so by selecting a course by Kieran Keene, titled &lt;a href="https://www.udemy.com/course/unleash-your-polars-python-skills-in-just-2-hours/"&gt;Learn Data Analytics with Polars (Python) in just 2 hours!&lt;/a&gt;. I got this course during another of Udemy's sales for $13.&lt;/p&gt;
&lt;h2 id="about-the-course"&gt;About the course&lt;a class="headerlink" href="#about-the-course" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The entire course is run out of &lt;a href="https://colab.research.google.com/"&gt;Google Colab&lt;/a&gt;. This is nice, because you don't need to set up a virtual environment or install anything to take this course. &lt;/p&gt;
&lt;p&gt;The course is taught with Polars version &lt;code&gt;0.17.3&lt;/code&gt;, which is from April 2023. I took this course in January 2024, so I used the current version - &lt;code&gt;0.20.2&lt;/code&gt;. This introduces a few very minor deprecations from the older version, but simply reading the deprecation warning for each tells you how to solve the problem.&lt;/p&gt;
&lt;p&gt;Namely, the two deprecations that I recall running into:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;In 0.17 there is the &lt;code&gt;df.apply()&lt;/code&gt; function. This has been deprecated in favor of &lt;code&gt;df.map_rows()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;groupby()&lt;/code&gt; function has been deprecated in favor of &lt;code&gt;group_by()&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Both take exactly the same arguments, so it was as simple as renaming the instructor's function to the modern one. &lt;/p&gt;
&lt;p&gt;The one other minor difference is that the example data has changed at some point between when the course material was recorded and when I took the course. This doesn't change how any of the lectures behave or any of the examples act. It does change a couple results, so I couldn't compare exact numbers between what I received and what the instructor received.&lt;/p&gt;
&lt;p&gt;This was not a problem though. The instructor does a very good job of explaining what each lecture is going to teach, shows at least one method of accomplishing the task, and then summarizing the lecture. Through this, it's easy to determine if the results you get with the different sample data returns an accurate result.&lt;/p&gt;
&lt;p&gt;I enjoyed the short lectures. I believe the longest single lecture is 11 minutes long, but each builds off of what you've done previously. By introducing these natural break points, it's easy to complete a topic and then spend a couple minutes experimenting with other methods or logic to see how the library behaves.&lt;/p&gt;
&lt;p&gt;Additionally, there are multiple ways to perform certain tasks, and the instructor takes the time to explain each of these. Building off of one another, it's easy to determine which is appropriate for the scenario being discussed.&lt;/p&gt;
&lt;p&gt;Speaking of scenarios, the course concludes with two short challenges. The goal is to use the knowledge gained from previous lectures to figure out results to two problems. If you followed along through the hour and a half course, these should be pretty easy to figure out but do require combining multiple steps to get to the answer.&lt;/p&gt;
&lt;h2 id="conclusion"&gt;Conclusion&lt;a class="headerlink" href="#conclusion" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;I enjoyed this course, the instructor's teaching method, and two challenges at the end to tie the course back to the lectures. This was a very hands on course and the breaks between lectures and sections encouraged experimenting with the library. &lt;/p&gt;
&lt;p&gt;Experimenting helps me learn more about the tool, and I commend the instructor for building the course in such a way that students could try out the library.&lt;/p&gt;
&lt;p&gt;I also found that I liked the Polars library. Pandas has its quirks and when I dig more into Polars, I'm sure I'll find it has some too. But, Polars feels easier to grasp. The API is easier to understand as you are reading through the code. &lt;/p&gt;
&lt;p&gt;I think I'm going to try out another course to see what else the library can do. That alone should be a compliment to both the library and this course - it's encouraged me to keep learning more.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://www.udemy.com/certificate/UC-28b98b47-28aa-47f8-9525-f9d85a7ccc2d/"&gt;&lt;img alt="Learn Data Analytics with Polars (Python) in just 2 hours! Completion Certificate" src="https://andrewwegner.com/images/udemy-learn-analytics-polars.jpg"/&gt;&lt;/a&gt;&lt;/p&gt;</content><category term="Review"/><category term="review"/><category term="technical"/><category term="learning"/></entry><entry><title>Review of Learn Github Actions for CI/CD DevOps Pipelines Course</title><link href="https://andrewwegner.com/github-actions-cicd-pipelines-review.html" rel="alternate"/><published>2024-01-22T10:30:00-06:00</published><updated>2024-01-22T10:30:00-06:00</updated><author><name>Andy Wegner</name></author><id>tag:andrewwegner.com,2024-01-22:/github-actions-cicd-pipelines-review.html</id><summary type="html">&lt;p&gt;A review of the Udemy course: Learn Github Actions for CI/CD DevOps Pipelines&lt;/p&gt;</summary><content type="html">
&lt;h2 id="introduction"&gt;Introduction&lt;a class="headerlink" href="#introduction" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;I received a coupon code for Houssem Dellai's &lt;a href="https://www.udemy.com/course/learn-github-actions-ci-cd-devops-pipelines/"&gt;Learn Github Actions for CI/CD DevOps Pipelines Course&lt;/a&gt;. I've written about &lt;a href="https://andrewwegner.com/setting-up-gitlab-runners.html"&gt;GitLab runners&lt;/a&gt; and the CI/CD process of that tool in the past. I've used &lt;a href="https://andrewwegner.com/find-broken-links-with-github-actions.html"&gt;GitHub actions to check this blog for broken links&lt;/a&gt;. I've used &lt;a href="https://andrewwegner.com/how-i-set-up-openshift-travisci-and-flask.html"&gt;TravisCI&lt;/a&gt; (and written &lt;a href="https://andrewwegner.com/travisci-insecure-environment-variables.html"&gt;why not to trust TravisCI&lt;/a&gt;). I've also used BitBucket Pipelines in my professional life. I wanted to learn a bit more about what GitHub Actions could do and the coupon code made this 4 hour course worth the time to investigate.&lt;/p&gt;
&lt;p&gt;You'll see below in more detail, but this course was not what I expected. It's advertised as being for "All beginners" and "Ops experts". I argue that both of these are incorrect. This course felt more like a 20-30 lecture "GitHub Actions Cookbook" than a course looking to &lt;em&gt;explain&lt;/em&gt; GitHub actions.&lt;/p&gt;
&lt;p&gt;Let's go through it...&lt;/p&gt;
&lt;h2 id="course-overview"&gt;Course Overview&lt;a class="headerlink" href="#course-overview" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;There are multiple formats the instructor uses in this course. It's pretty obvious that the sections in this lecture were recorded independently. Some lectures take place entirely within the GitHub interface, some are using Powerpoint slides to drive the lecture, one is done on a whiteboard. To me, it seems the various lectures have been bundled together. &lt;/p&gt;
&lt;p&gt;After a bit of research - and a mid-lecture "Check out my YouTube" - it's clear why that feeling exists. The &lt;a href="https://www.youtube.com/@HoussemDellai/videos"&gt;lectures are available on YouTube&lt;/a&gt;. Not all of the videos on YouTube are part of this course, but scrolling through, I can quickly see that many of the videos in the course are on YouTube.&lt;/p&gt;
&lt;p&gt;As for the content of the course, it doesn't match what I was expecting. With the "All beginners" advertisement, I was expecting more discussion and coverage on &lt;em&gt;why&lt;/em&gt; certain things were being done. A discussion on the trade offs or the instructor's thought process on how to accomplish tasks.&lt;/p&gt;
&lt;p&gt;Instead, the content is a lot of "this is my code to accomplish this task". It is a great course to get the quick run down of how to accomplish tasks, especially with Azure. However, it is not a good course if you want to know why a specific block of code is the best route to go. It's a cookbook to accomplish a task. A handful of lectures do cover theory, but these are few and very far between.&lt;/p&gt;
&lt;h2 id="conclusion"&gt;Conclusion&lt;a class="headerlink" href="#conclusion" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;If my coupon hadn't knocked off the entire price of the course, I'd be upset with this one. I also picked up two other courses by this instructor and I suspect I'm going to find similar concerns with those courses. Honestly, that's too bad, because the content is good. It's just not marketed how it's packaged. With my expectations set for learning a bit, it's disappointing to get lectures that are designed to be copy and pasted from.&lt;/p&gt;
&lt;p&gt;Don't come into this expecting to learn a lot of reasoning behind how things are accomplished. If you need an overview of how to accomplish the tasks in the content list though, you'll find that. It'll be a relatively dry walkthrough of the exact code you need. But, if you want to know why...this isn't the course for you.&lt;/p&gt;
&lt;p&gt;If you can find a coupon code for this, I think it's worth your time to see how some of these advanced tasks are accomplished. If you can't find that coupon though, head over to the &lt;a href="https://www.youtube.com/@HoussemDellai/videos"&gt;instructor's YouTube page&lt;/a&gt; and watch that section for free. &lt;/p&gt;
&lt;p&gt;&lt;a href="https://www.udemy.com/certificate/UC-628403d3-7a5b-46b5-8083-32d915e79471/"&gt;&lt;img alt="Learn Github Actions for CI/CD DevOps Pipelines Completion Certificate" src="https://andrewwegner.com/images/udemy-github-actions-pipeline-review.jpg"/&gt;&lt;/a&gt;&lt;/p&gt;</content><category term="Review"/><category term="review"/><category term="technical"/><category term="learning"/></entry><entry><title>Python Gotcha: Modifying a list while iterating</title><link href="https://andrewwegner.com/python-gotcha-modify-list-while-iterating.html" rel="alternate"/><published>2024-01-03T15:00:00-06:00</published><updated>2024-01-03T15:00:00-06:00</updated><author><name>Andy Wegner</name></author><id>tag:andrewwegner.com,2024-01-03:/python-gotcha-modify-list-while-iterating.html</id><summary type="html">&lt;p&gt;Python makes it easy to modify a list while you are iterating through it's elements. This will bite you. Read on to find out how and what can be done about it.&lt;/p&gt;</summary><content type="html">
&lt;h2 id="introduction"&gt;Introduction&lt;a class="headerlink" href="#introduction" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;A fairly common task an engineer may need to do is to remove all numbers from a list below a threshold. It's a simple enough task that anyone could accomplish - iterate over the list, check the value, if it's below the threshold remove it. With Python you can do this in a &lt;code&gt;for&lt;/code&gt; loop.&lt;/p&gt;
&lt;h2 id="gotcha"&gt;Gotcha&lt;a class="headerlink" href="#gotcha" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Let's check code for this simple problem.&lt;/p&gt;
&lt;div class="codehilight code"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;numbers_list&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="n"&gt;0, 9, 2, 1, 5, 1, 10, 4, 3&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;index&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;value&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;numbers_list&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;index&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;value&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'Deleted'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;value&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'Kept'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;value&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;del&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;numbers_list&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;numbers_list&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;It'd be reasonable to expect a final list that looks like this:&lt;/p&gt;
&lt;div class="codehilight code"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;[9, 5, 10]
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;But, the actual output is:&lt;/p&gt;
&lt;div class="codehilight code"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;[9, 1, 5, 10, 3]
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;What is &lt;em&gt;happening&lt;/em&gt;? &lt;/p&gt;
&lt;h2 id="explanation"&gt;Explanation&lt;a class="headerlink" href="#explanation" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The (unconscious?) assumption that the developer is making with this piece of code is that the final list &lt;em&gt;must&lt;/em&gt; be in &lt;code&gt;numbers_list&lt;/code&gt;. Since we're able to iterate and modify, there is no need for a temporary variable or a need to worry about &lt;a href="https://andrewwegner.com/python-gotcha-list-copy.html"&gt;list copy gotchas&lt;/a&gt;. But, let's go through this one iteration at a time.&lt;/p&gt;
&lt;div class="codehilight code"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nv"&gt;Starting&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;list&lt;/span&gt;:&lt;span class="w"&gt; &lt;/span&gt;[&lt;span class="mi"&gt;0&lt;/span&gt;,&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;9&lt;/span&gt;,&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;,&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;,&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;,&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;,&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;,&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;,&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;]

&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;Iteration&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;Index&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;Value&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;Value&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;?&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;Action&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;List&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;after&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;iteration&lt;/span&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;---------&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-----&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-----&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;----------&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;------&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-------------------------&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="w"&gt;         &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;True&lt;/span&gt;&lt;span class="w"&gt;       &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;Delete&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;[&lt;span class="mi"&gt;9&lt;/span&gt;,&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;,&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;,&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;,&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;,&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;,&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;,&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;]&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt;         &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;True&lt;/span&gt;&lt;span class="w"&gt;       &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;Delete&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;[&lt;span class="mi"&gt;9&lt;/span&gt;,&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;,&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;,&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;,&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;,&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;,&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;]&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="w"&gt;         &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;False&lt;/span&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;Keep&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;[&lt;span class="mi"&gt;9&lt;/span&gt;,&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;,&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;,&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;,&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;,&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;,&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;]&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="w"&gt;         &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;True&lt;/span&gt;&lt;span class="w"&gt;       &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;Delete&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;[&lt;span class="mi"&gt;9&lt;/span&gt;,&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;,&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;,&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;,&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;,&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;]&lt;span class="w"&gt;       &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="w"&gt;         &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;True&lt;/span&gt;&lt;span class="w"&gt;       &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;Delete&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;[&lt;span class="mi"&gt;9&lt;/span&gt;,&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;,&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;,&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;,&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;]&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;

&lt;span class="k"&gt;End&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;of&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;iteration&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;as&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;list&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;has&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;no&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;more&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;elements&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Looking at this, it's a little clearer what's going on. Let's go through these one iteration at a time.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;On the first iteration, the value of &lt;code&gt;Index 0&lt;/code&gt; is &lt;code&gt;0&lt;/code&gt;, so it's deleted. &lt;em&gt;And the problem starts now&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;On the next iteration, the value of &lt;code&gt;Index 1&lt;/code&gt; is &lt;code&gt;2&lt;/code&gt; (&lt;strong&gt;not 9&lt;/strong&gt;). In the previous iteration, &lt;code&gt;Index 0&lt;/code&gt; was deleted, which means everything has moved up one index. The value of &lt;code&gt;9&lt;/code&gt; was never checked. Instead, we look at a value of &lt;code&gt;2&lt;/code&gt;. It's less than our threshold so it's also deleted.&lt;/li&gt;
&lt;li&gt;On the third iteration, the value of &lt;code&gt;Index 2&lt;/code&gt; is &lt;code&gt;5&lt;/code&gt;. Again, we've skipped a value (&lt;code&gt;1&lt;/code&gt;) and it's never even checked. This iteration doesn't result in a deletion because &lt;code&gt;5&lt;/code&gt; is not less than &lt;code&gt;5&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;On the fourth iteration, the value of &lt;code&gt;Index 3&lt;/code&gt; is &lt;code&gt;1&lt;/code&gt;. This is deleted as expected.&lt;/li&gt;
&lt;li&gt;On the fifth iteration, the value of &lt;code&gt;Index 4&lt;/code&gt; is &lt;code&gt;4&lt;/code&gt;. We've skipped another value (&lt;code&gt;10&lt;/code&gt;) and this iteration's value is less than our threshold so it's deleted. &lt;/li&gt;
&lt;li&gt;There is no further iteration, because the last one has made the final value the last index, so the final value is never checked. &lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;We skipped checking several values because we messed with indexes that were either being operated on or hadn't been operated on yet. This results in unexpected behavior.&lt;/p&gt;
&lt;h2 id="solution"&gt;Solution&lt;a class="headerlink" href="#solution" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;A quick solution is to add the values that exceed our threshold to another variable:&lt;/p&gt;
&lt;div class="codehilight code"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;numbers_list&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;larger_list&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;numbers_list&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;larger_list&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;larger_list&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This results in the expected list of &lt;code&gt;[9, 5, 10]&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Another solution, assuming that the result &lt;em&gt;needs&lt;/em&gt; to sit in a variable with the same name, is to use a list comprehension. This does the same thing as above, but keeps the code more compact. It also works because the final result is assigned to a new variable object with the same name.&lt;/p&gt;
&lt;div class="codehilight code"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;numbers_list = [0, 9, 2, 1, 5, 1, 10, 4, 3]
numbers_list = [nums for nums in numbers_list if nums &amp;gt;= 5]
print(numbers_list)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This results in the expected list of &lt;code&gt;[9, 5, 10]&lt;/code&gt;&lt;/p&gt;</content><category term="Technical Solutions"/><category term="technical"/></entry><entry><title>Python Gotcha: List Copy Problems</title><link href="https://andrewwegner.com/python-gotcha-list-copy.html" rel="alternate"/><published>2023-12-20T15:30:00-06:00</published><updated>2023-12-20T15:30:00-06:00</updated><author><name>Andy Wegner</name></author><id>tag:andrewwegner.com,2023-12-20:/python-gotcha-list-copy.html</id><summary type="html">&lt;p&gt;Copying a Python list (or any mutable object) isn't as simple as setting one equal to another. Let's discuss a better way to do this.&lt;/p&gt;</summary><content type="html">
&lt;h2 id="introduction"&gt;Introduction&lt;a class="headerlink" href="#introduction" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;This problem isn't limited to only &lt;code&gt;list&lt;/code&gt;s. Any mutable object in Python can be impacted by this "gotcha". The problem is setting one mutable object equal to another doesn't make a copy. It assigns a new variable to the same object as the original. Let's take a look and see how this appears.&lt;/p&gt;
&lt;h2 id="the-first-gotcha"&gt;The First Gotcha&lt;a class="headerlink" href="#the-first-gotcha" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The first example is pretty simple. Let's make a copy of our list&lt;/p&gt;
&lt;div class="codehilight code"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&amp;gt;&amp;gt;&amp;gt; list1 = [1,2,3,4,5]
&amp;gt;&amp;gt;&amp;gt; list2 = list1
&amp;gt;&amp;gt;&amp;gt; list1
[1, 2, 3, 4, 5]
&amp;gt;&amp;gt;&amp;gt; list2
[1, 2, 3, 4, 5]
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;That looks like I have two lists, both with the same set of values. But, let's modify the second list and change the first element in the list to be &lt;code&gt;6&lt;/code&gt;&lt;/p&gt;
&lt;div class="codehilight code"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&amp;gt;&amp;gt;&amp;gt; list2
[6, 2, 3, 4, 5]
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;But what about &lt;code&gt;list1&lt;/code&gt;? It also has changed.&lt;/p&gt;
&lt;div class="codehilight code"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&amp;gt;&amp;gt;&amp;gt; list1
[6, 2, 3, 4, 5]
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This is occurring because, as mentioned, &lt;code&gt;list2&lt;/code&gt; is assigned to the same object as &lt;code&gt;list1&lt;/code&gt;. Any change to either, changes the object that both are assigned to.&lt;/p&gt;
&lt;div class="codehilight code"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&amp;gt;&amp;gt;&amp;gt; print(f"list1 id: {id(list1)}")
list1 id: 1497534800448
&amp;gt;&amp;gt;&amp;gt; print(f"list2 id: {id(list2)}")
list2 id: 1497534800448
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2 id="the-first-solution"&gt;The First Solution&lt;a class="headerlink" href="#the-first-solution" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;This can be solved a few different ways. The important take away, though, is that you have to create a new object with the original values. I can think of a few ways to handle this. This article doesn't consider performance for these basic lists.&lt;/p&gt;
&lt;p&gt;One option is to utilize the &lt;code&gt;list()&lt;/code&gt; method and pass it the initial values from &lt;code&gt;list1&lt;/code&gt;&lt;/p&gt;
&lt;div class="codehilight code"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&amp;gt;&amp;gt;&amp;gt; list3 = list(list1)
&amp;gt;&amp;gt;&amp;gt; list3
[6, 2, 3, 4, 5]
&amp;gt;&amp;gt;&amp;gt; list3[0] = 0
&amp;gt;&amp;gt;&amp;gt; list3
[0, 2, 3, 4, 5]
&amp;gt;&amp;gt;&amp;gt; list1
[6, 2, 3, 4, 5]
&amp;gt;&amp;gt;&amp;gt; print(f"list1 id: {id(list1)}")
list1 id: 1497534800448
&amp;gt;&amp;gt;&amp;gt; print(f"list3 id: {id(list3)}")
list3 id: 1497531282368
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;A second option is to &lt;code&gt;slice()&lt;/code&gt; the entire original list into a new variable.&lt;/p&gt;
&lt;div class="codehilight code"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&amp;gt;&amp;gt;&amp;gt; list4 = list1[:]
&amp;gt;&amp;gt;&amp;gt; list4
[6, 2, 3, 4, 5]
&amp;gt;&amp;gt;&amp;gt; list4[0] = 0
&amp;gt;&amp;gt;&amp;gt; list4
[0, 2, 3, 4, 5]
&amp;gt;&amp;gt;&amp;gt; list1
[6, 2, 3, 4, 5]
&amp;gt;&amp;gt;&amp;gt; print(f"list1 id: {id(list1)}")
list1 id: 1497534800448
&amp;gt;&amp;gt;&amp;gt; print(f"list4 id: {id(list4)}")
list4 id: 1497534798208
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Lastly, the &lt;code&gt;list&lt;/code&gt; object has a &lt;code&gt;copy()&lt;/code&gt; method that you can utilize. &lt;/p&gt;
&lt;div class="codehilight code"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&amp;gt;&amp;gt;&amp;gt; list5 = list1.copy()
&amp;gt;&amp;gt;&amp;gt; list5[0] = 0
&amp;gt;&amp;gt;&amp;gt; list5
[0, 2, 3, 4, 5]
&amp;gt;&amp;gt;&amp;gt; list1
[6, 2, 3, 4, 5]
&amp;gt;&amp;gt;&amp;gt; print(f"list1 id: {id(list1)}")
list1 id: 1497534800448
&amp;gt;&amp;gt;&amp;gt; print(f"list5 id: {id(list5)}")
list5 id: 1497534799552
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;With each of these three options, you can see that we created new objects because each has a unique object id. Changing one of these copies does not change the original's values.&lt;/p&gt;
&lt;h2 id="the-second-gotcha"&gt;The Second Gotcha&lt;a class="headerlink" href="#the-second-gotcha" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;You, a savvy developer, know all this though. You also have a more advanced data structure. You have a &lt;em&gt;list of lists&lt;/em&gt;. &lt;/p&gt;
&lt;div class="codehilight code"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;[
    ['first element', 1, 2, 3],
    ['second element', 4, 5, 6],
    ['third element', 7, 8, 9],
]
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Since you know about this gotcha, you are going to make a new object and copy the values to the new object.&lt;/p&gt;
&lt;div class="codehilight code"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&amp;gt;&amp;gt;&amp;gt; deeplist1 = [
...     ['first element', 1, 2, 3],
...     ['second element', 4, 5, 6],
...     ['third element', 7, 8, 9],
... ]
&amp;gt;&amp;gt;&amp;gt; deeplist2 = deeplist1.copy()
&amp;gt;&amp;gt;&amp;gt; print(f"deeplist1 id: {id(deeplist1)}")
deeplist1 id: 1497534571008
&amp;gt;&amp;gt;&amp;gt; print(f"deeplist2 id: {id(deeplist2)}")
deeplist2 id: 1497534577536
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Now, with two different objects, is it safe to modify an element in one of the sublists? Turns out, it is not.&lt;/p&gt;
&lt;div class="codehilight code"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&amp;gt;&amp;gt;&amp;gt; deeplist2[0][0] = "NOT 3RD"
&amp;gt;&amp;gt;&amp;gt; deeplist2
[['NOT 3RD', 1, 2, 3], ['second element', 4, 5, 6], ['third element', 7, 8, 9]]
&amp;gt;&amp;gt;&amp;gt; deeplist1
[['NOT 3RD', 1, 2, 3], ['second element', 4, 5, 6], ['third element', 7, 8, 9]]
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;But why? The ids of &lt;code&gt;deeplist1&lt;/code&gt; and &lt;code&gt;deeplist2&lt;/code&gt; are different. This is true, but the sublists are not. Each sublist is assigned the same object id as the original.&lt;/p&gt;
&lt;div class="codehilight code"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&amp;gt;&amp;gt;&amp;gt; id(deeplist1[0][0]), id(deeplist2[0][0])
(1497534796864, 1497534796864)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2 id="the-second-solution"&gt;The Second Solution&lt;a class="headerlink" href="#the-second-solution" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Utilize the &lt;a href="https://docs.python.org/3/library/copy.html#copy.deepcopy"&gt;&lt;code&gt;deepcopy()&lt;/code&gt;&lt;/a&gt; method to make a copy of your original object.&lt;/p&gt;
&lt;div class="codehilight code"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;copy&lt;/span&gt;
&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;deeplist3&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;copy&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;deepcopy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;deeplist1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;deeplist3&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"NEW VALUE"&lt;/span&gt;
&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;deeplist3&lt;/span&gt;
&lt;span class="p"&gt;[[&lt;/span&gt;&lt;span class="s1"&gt;'NEW VALUE'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'second element'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'third element'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt;
&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;deeplist1&lt;/span&gt;
&lt;span class="p"&gt;[[&lt;/span&gt;&lt;span class="s1"&gt;'NOT 3RD'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'second element'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'third element'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Using &lt;code&gt;deepcopy()&lt;/code&gt; we can safely make a copy of our list of lists and modify both the original and the copy without impacting the other.&lt;/p&gt;</content><category term="Technical Solutions"/><category term="technical"/></entry><entry><title>Python Gotcha: Timezone Naive Functions like utcnow() and utcfromtimestamp()</title><link href="https://andrewwegner.com/python-gotcha-timezone-naive-functions.html" rel="alternate"/><published>2023-11-23T10:15:00-06:00</published><updated>2023-11-23T10:15:00-06:00</updated><author><name>Andy Wegner</name></author><id>tag:andrewwegner.com,2023-11-23:/python-gotcha-timezone-naive-functions.html</id><summary type="html">&lt;p&gt;utcnow() and utcfromtimestamp() don't know about timezones and that causes problems. Let's talk about those, how to fix the problem and their recent deprecation in Python 3.12&lt;/p&gt;</summary><content type="html">
&lt;h2 id="introduction"&gt;Introduction&lt;a class="headerlink" href="#introduction" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;There are two common functions that are &lt;a href="https://docs.python.org/3/library/datetime.html#aware-and-naive-objects"&gt;naive datetime objects&lt;/a&gt;. The idea behind these types of objects is that timezones don't matter. Perhaps you always assume that datetimes are in a set timezone. Or, you are aware of timezones and want to make things easy on yourself so always assume datetimes are set to UTC.&lt;/p&gt;
&lt;p&gt;This is your mistake. &lt;/p&gt;
&lt;p&gt;The &lt;code&gt;utcnow()&lt;/code&gt; and &lt;code&gt;utcfromtimestamp()&lt;/code&gt; functions are naive, but other &lt;code&gt;datetime&lt;/code&gt; functions are not.&lt;/p&gt;
&lt;h2 id="the-gotcha"&gt;The Gotcha&lt;a class="headerlink" href="#the-gotcha" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;div class="codehilight code"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;datetime&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;
&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;dt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utcfromtimestamp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;dt&lt;/span&gt;
&lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1970&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;dt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="mf"&gt;21600.0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;What happened here? &lt;/p&gt;
&lt;p&gt;This was run on a computer in the Central Standard Time time zone. First, I created an object set to the UNIX epoch - Jan. 1, 1970. Since I utilized &lt;code&gt;utcfromtimestamp()&lt;/code&gt; to do this, there is no timezone data attached to this object. When I use the standard &lt;code&gt;timestamp()&lt;/code&gt; function, Python sees there is no timestamp to utilize, so it uses the system configured timezone. Hence the 21,600 second offset from the epoch.&lt;/p&gt;
&lt;p&gt;This didn't always happen though. Sometime &lt;a href="https://github.com/python/cpython/issues/81669"&gt;between Python 2 and Python 3 this behavior changed&lt;/a&gt;. This assumption that "no timezone means local timezone" introduced this gotcha. In my opinion this is a rather dangerous one too.&lt;/p&gt;
&lt;h2 id="solution"&gt;Solution&lt;a class="headerlink" href="#solution" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Utilize explicit time stamps everywhere. From &lt;a href="https://peps.python.org/pep-0020/"&gt;The Zen of Python - PEP 20&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Explicit is better than implicit. &lt;/p&gt;
&lt;/blockquote&gt;
&lt;div class="codehilight code"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;datetime&lt;/span&gt;
&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;dt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fromtimestamp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UTC&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;dt&lt;/span&gt;
&lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1970&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tzinfo&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;timezone&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;dt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="mf"&gt;0.0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Notice that by adding &lt;code&gt;datetime.UTC&lt;/code&gt; to my &lt;code&gt;fromtimestamp()&lt;/code&gt; call, I get a datetime object with &lt;code&gt;tzinfo&lt;/code&gt;. This makes the conversion from the datetime object back into a timestamp safe. &lt;/p&gt;
&lt;p&gt;This is the way that Python is moving. In an effort to correct these unsafe assumptions that were introduced early in Python 3, the naive datetime functions have been deprecated. If you were to run my first block of code in Python 3.12, you receive a deprecation warning. &lt;/p&gt;
&lt;div class="codehilight code"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;dt&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;datetime&lt;/span&gt;.&lt;span class="nv"&gt;utcfromtimestamp&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nv"&gt;stdin&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;:&lt;span class="mi"&gt;1&lt;/span&gt;:&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;DeprecationWarning&lt;/span&gt;:&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;datetime&lt;/span&gt;.&lt;span class="nv"&gt;datetime&lt;/span&gt;.&lt;span class="nv"&gt;utcfromtimestamp&lt;/span&gt;&lt;span class="ss"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;is&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;deprecated&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;and&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;scheduled&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;removal&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;a&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;future&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;version&lt;/span&gt;.&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;Use&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;timezone&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nv"&gt;aware&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;objects&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;represent&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;datetimes&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;UTC&lt;/span&gt;:&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;datetime&lt;/span&gt;.&lt;span class="nv"&gt;datetime&lt;/span&gt;.&lt;span class="nv"&gt;fromtimestamp&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;timestamp&lt;/span&gt;,&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;datetime&lt;/span&gt;.&lt;span class="nv"&gt;UTC&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;.
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Notice that the message is telling you to add the timezone information and to utilize &lt;code&gt;fromtimestamp()&lt;/code&gt;. In the coming year - years? - both the commonly seen &lt;code&gt;utcnow()&lt;/code&gt; and &lt;code&gt;utcfromtimestamp()&lt;/code&gt; will be removed.&lt;/p&gt;
&lt;p&gt;In my opinion, as someone that has fought this bug in the past, it's a welcome change. Timezone assumptions are difficult to troubleshoot. Assumptions about underlying behavior make it a challenge. Running the same script in different timezones, like you would with a geographically diverse team, makes it even harder. It's especially annoying if one of your coworkers is utilizing UTC as their time zone so that the script behaves properly for them but no others.&lt;/p&gt;
&lt;p&gt;I have code bases that will need to make this change, but in the end, they will be easier to maintain with the removal of the &lt;code&gt;utc*&lt;/code&gt; functions.&lt;/p&gt;</content><category term="Technical Solutions"/><category term="technical"/></entry><entry><title>Ruby Gotcha: Operator Precedence</title><link href="https://andrewwegner.com/ruby-gotcha-operator-precedence.html" rel="alternate"/><published>2023-10-23T10:45:00-05:00</published><updated>2023-10-23T10:45:00-05:00</updated><author><name>Andy Wegner</name></author><id>tag:andrewwegner.com,2023-10-23:/ruby-gotcha-operator-precedence.html</id><summary type="html">&lt;p&gt;In Ruby you can use either &lt;code&gt;and&lt;/code&gt; or &lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt;. You can use &lt;code&gt;or&lt;/code&gt; or &lt;code&gt;||&lt;/code&gt;. What is the difference and what's the catch?&lt;/p&gt;</summary><content type="html">
&lt;h2 id="introduction"&gt;Introduction&lt;a class="headerlink" href="#introduction" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;In one of my previous roles, the team utilized Ruby. I was not an individual contributor at that point in my career, so my exposure to it was less than much of my team. However, during my tenure, I noticed several "Gotchas" of the Ruby language. Each language has these in their own way, but in this post I'm going to cover one specific one.&lt;/p&gt;
&lt;p&gt;What's the difference between &lt;code&gt;and&lt;/code&gt; and &lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt;? What about the difference between &lt;code&gt;or&lt;/code&gt; and &lt;code&gt;||&lt;/code&gt;?&lt;/p&gt;
&lt;h2 id="the-gotcha"&gt;The Gotcha&lt;a class="headerlink" href="#the-gotcha" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;At first pass, these two blocks of code appear to do the same thing. Let's take a look.&lt;/p&gt;
&lt;div class="codehilight code"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;irb(main):003:0&amp;gt; true &amp;amp;&amp;amp; false
=&amp;gt; false
irb(main):004:0&amp;gt; true and false
=&amp;gt; false
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;By the rules of boolean logic, both &lt;code&gt;and&lt;/code&gt; and &lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt; statements return &lt;code&gt;false&lt;/code&gt;.&lt;/p&gt;
&lt;div class="codehilight code"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;irb(main):005:0&amp;gt; true || false
=&amp;gt; true
irb(main):006:0&amp;gt; true or false
=&amp;gt; true
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Boolean logic holds true for the &lt;code&gt;or&lt;/code&gt; and &lt;code&gt;||&lt;/code&gt; statements and return &lt;code&gt;true&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;But, what happens if we assign the result of this to a variable?&lt;/p&gt;
&lt;div class="codehilight code"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;irb(main):007:0&amp;gt; result = true and false
=&amp;gt; false
irb(main):008:0&amp;gt; result1 = true &amp;amp;&amp;amp; false
=&amp;gt; false
irb(main):009:0&amp;gt; result
=&amp;gt; true
irb(main):010:0&amp;gt; result1
=&amp;gt; false
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Now we see the problem. With the &lt;code&gt;and&lt;/code&gt; operator, &lt;code&gt;result = true&lt;/code&gt;. With the &lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt; operator, &lt;code&gt;result1 = false&lt;/code&gt;. &lt;/p&gt;
&lt;p&gt;Looking at the &lt;code&gt;or&lt;/code&gt;/&lt;code&gt;||&lt;/code&gt; operators quickly,&lt;/p&gt;
&lt;div class="codehilight code"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;irb(main):011:0&amp;gt; result2 = true or false
=&amp;gt; true
irb(main):012:0&amp;gt; result3 = true || false
=&amp;gt; true
irb(main):013:0&amp;gt; result2
=&amp;gt; true
irb(main):014:0&amp;gt; result3
=&amp;gt; true
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This all looks reasonable. But, let's swap the order of our &lt;code&gt;true&lt;/code&gt;/&lt;code&gt;false&lt;/code&gt; in the statement.&lt;/p&gt;
&lt;div class="codehilight code"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;irb(main):015:0&amp;gt; result2 = false or true
=&amp;gt; true
irb(main):016:0&amp;gt; result3 = false || true
=&amp;gt; true
irb(main):017:0&amp;gt; result2
=&amp;gt; false
irb(main):018:0&amp;gt; result3
=&amp;gt; true
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Here we see the differences again, with the &lt;code&gt;or&lt;/code&gt; operator setting &lt;code&gt;result2 = false&lt;/code&gt; and the &lt;code&gt;||&lt;/code&gt; operator setting &lt;code&gt;result3 = true&lt;/code&gt;&lt;/p&gt;
&lt;h2 id="what-is-going-on"&gt;What is going on?&lt;a class="headerlink" href="#what-is-going-on" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;and&lt;/code&gt; and &lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt; / &lt;code&gt;or&lt;/code&gt; and &lt;code&gt;||&lt;/code&gt; are the same thing, but they have different orders of precedence. The (not well formatted in my opinion) documentation shows the &lt;a href="https://docs.ruby-lang.org/en/3.2/syntax/precedence_rdoc.html"&gt;precedence table for Ruby&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The key things to point out here:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt; and &lt;code&gt;||&lt;/code&gt; are above &lt;code&gt;and&lt;/code&gt; and &lt;code&gt;or&lt;/code&gt;, meaning they will be evaluated first.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;=&lt;/code&gt; is also above &lt;code&gt;and&lt;/code&gt; and &lt;code&gt;or&lt;/code&gt; (but below &lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt; and &lt;code&gt;||&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Let's look at one of the examples again:&lt;/p&gt;
&lt;div class="codehilight code"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;irb(main):015:0&amp;gt; result2 = false or true
=&amp;gt; true
irb(main):017:0&amp;gt; result2
=&amp;gt; false
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;With the order of precedence in mind, this is what happens:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;result2&lt;/code&gt; is assigned the value of &lt;code&gt;false&lt;/code&gt; because &lt;code&gt;=&lt;/code&gt; occurs before &lt;code&gt;or&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;The remainder of the statement is evaluated as &lt;code&gt;true&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This was hidden when &lt;code&gt;true&lt;/code&gt; and &lt;code&gt;false&lt;/code&gt; was swapped because &lt;code&gt;result2&lt;/code&gt; was assigned a value of &lt;code&gt;true&lt;/code&gt; and then the remainder of the statement was also evaluated to &lt;code&gt;true&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id="how-do-you-prevent-this"&gt;How do you prevent this?&lt;a class="headerlink" href="#how-do-you-prevent-this" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;There are very few reasons to utilize the English words &lt;code&gt;and&lt;/code&gt; and &lt;code&gt;or&lt;/code&gt; in Ruby's logical evaluations. Instead, these should be utilized as flow control modifiers (think &lt;code&gt;if&lt;/code&gt; and &lt;code&gt;unless&lt;/code&gt;) and not for boolean logic. &lt;/p&gt;
&lt;p&gt;Here's an example of how it should be utilized for flow control&lt;/p&gt;
&lt;div class="codehilight code"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;irb&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;&lt;span class="mi"&gt;019&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;numeral&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;numeral&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;irb&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;&lt;span class="mi"&gt;19&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="ow"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;`&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="s1"&gt;': undefined method `/'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;NilClass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NoMethodError&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;C&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;Ruby32&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;x64&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;lib&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;ruby&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;gems&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mf"&gt;3.2&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;gems&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;irb&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;1.6&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;exe&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;irb&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="ow"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;`&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;top&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;required&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;C&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;Ruby32&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;x64&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;bin&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;irb&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;33&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="ow"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;`&lt;/span&gt;&lt;span class="nb"&gt;load&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;C&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;Ruby32&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;x64&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;bin&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;irb&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;33&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="ow"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;`&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;
&lt;span class="n"&gt;irb&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;&lt;span class="mi"&gt;020&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;numeral&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;and&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;numeral&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;
&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;In our first example, with &lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt;, the statement is broken up like this&lt;/p&gt;
&lt;div class="codehilight code"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;numeral = (10 &amp;amp;&amp;amp; numeral) / 5
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This won't work because &lt;code&gt;numeral&lt;/code&gt; doesn't have a value when &lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt; is evaluated. Contrast this with the second version where the code:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Assigns a value of &lt;code&gt;10&lt;/code&gt; to &lt;code&gt;numeral&lt;/code&gt; &lt;/li&gt;
&lt;li&gt;Divides &lt;code&gt;numeral&lt;/code&gt; by &lt;code&gt;5&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;While basic, it should get the point across. Another example could be something like this:&lt;/p&gt;
&lt;div class="codehilight code"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;user = User.find_by_email(email) and user.send_email!
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Here, we are only sending an email to the user if we find their information. No information, no email is sent. &lt;/p&gt;</content><category term="Technical Solutions"/><category term="technical"/></entry><entry><title>Python Gotcha: Comparisons</title><link href="https://andrewwegner.com/python-gotcha-comparisons.html" rel="alternate"/><published>2023-10-18T10:15:00-05:00</published><updated>2023-10-18T10:15:00-05:00</updated><author><name>Andy Wegner</name></author><id>tag:andrewwegner.com,2023-10-18:/python-gotcha-comparisons.html</id><summary type="html">&lt;p&gt;Comparing two numerical variables in Python can have surprising results if you aren't aware of some common gotchas. This post covers a couple of the common ones.&lt;/p&gt;</summary><content type="html">
&lt;h2 id="float-equality-comparisons"&gt;Float Equality Comparisons&lt;a class="headerlink" href="#float-equality-comparisons" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Like every other programming language, Python can't accurately represent floating-point numbers. I'm sure many Computer Science students have lost many hours learning how &lt;a href="https://www.doc.ic.ac.uk/~eedwards/compsys/float/"&gt;floating representation works&lt;/a&gt;. I remember that class well.&lt;/p&gt;
&lt;p&gt;In any case, let's get into the problem with comparing &lt;code&gt;float&lt;/code&gt; values in Python and how to handle it.&lt;/p&gt;
&lt;div class="codehilight code"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&amp;gt;&amp;gt;&amp;gt; 0.1 + 0.2 == 0.3
False
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;You, me, and anyone with a few years of elementary school under their belt can see that this &lt;em&gt;should be&lt;/em&gt; &lt;code&gt;True&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;What's happening here? We can see the problem by breaking down the component parts of this comparison.&lt;/p&gt;
&lt;div class="codehilight code"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&amp;gt;&amp;gt;&amp;gt; 0.3
0.3
&amp;gt;&amp;gt;&amp;gt; 0.1 + 0.2
0.30000000000000004
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;And now we see the floating point representation that is causing a problem.&lt;/p&gt;
&lt;p&gt;So, how do we deal with this?&lt;/p&gt;
&lt;h3 id="decimal"&gt;Decimal&lt;a class="headerlink" href="#decimal" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;There are a couple options, both of which have their drawbacks. Let's start with &lt;a href="https://docs.python.org/3/library/decimal.html"&gt;&lt;code&gt;Decimal&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The decimal module provides support for fast correctly rounded decimal floating point arithmetic.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This sounds good, but an important gotcha here too is how it handles numerals vs. strings.&lt;/p&gt;
&lt;div class="codehilight code"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;decimal&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Decimal&lt;/span&gt;
&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'0.1000000000000000055511151231257827021181583404541015625'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'0.1'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'0.1'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This means, to accomplish the comparison above, we need to wrap each of our numbers in a string.&lt;/p&gt;
&lt;div class="codehilight code"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&amp;gt;&amp;gt;&amp;gt; Decimal('0.1') + Decimal('0.2') == Decimal('0.3')
True
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;That's annoying, but does function. &lt;/p&gt;
&lt;h3 id="isclose"&gt;isclose&lt;a class="headerlink" href="#isclose" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Python 3.5 implemented &lt;a href="https://peps.python.org/pep-0485/"&gt;PEP 485&lt;/a&gt; to test for approximate equality. This is done in the &lt;a href="https://docs.python.org/3/library/math.html#math.isclose"&gt;&lt;code&gt;isclose&lt;/code&gt;&lt;/a&gt; function. &lt;/p&gt;
&lt;div class="codehilight code"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;math&lt;/span&gt;
&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;math&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isclose&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.1&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mf"&gt;0.2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;0.3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kc"&gt;True&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;That's cleaner than wrapping everything in strings. But, it also is more verbose than a simple &lt;code&gt;==&lt;/code&gt; statement. It makes your code less clean, but does provide accurate comparisons.&lt;/p&gt;
&lt;h2 id="is-vs"&gt;is vs. ==&lt;a class="headerlink" href="#is-vs" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Another comparison that I've commonly seen misapplied is developers using &lt;code&gt;is&lt;/code&gt; when they mean &lt;code&gt;==&lt;/code&gt;. Put simply, &lt;code&gt;is&lt;/code&gt; should ONLY be used if you are checking if two references refer to the same object. &lt;code&gt;==&lt;/code&gt; is used to compare value by calling underlying &lt;code&gt;__eq__&lt;/code&gt; methods.&lt;/p&gt;
&lt;p&gt;Let's see this in action:&lt;/p&gt;
&lt;div class="codehilight code"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&amp;gt;&amp;gt;&amp;gt; a = [1, 2, 3]
&amp;gt;&amp;gt;&amp;gt; b = a
&amp;gt;&amp;gt;&amp;gt; c = [1, 2, 3]
&amp;gt;&amp;gt;&amp;gt; d = [3, 2, 1]
&amp;gt;&amp;gt;&amp;gt; a == b
True
&amp;gt;&amp;gt;&amp;gt; a == c
True
&amp;gt;&amp;gt;&amp;gt; a == d
False
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;So far, nothing unusual. &lt;code&gt;a&lt;/code&gt; has the same values of &lt;code&gt;b&lt;/code&gt; and &lt;code&gt;c&lt;/code&gt; and different values from &lt;code&gt;d&lt;/code&gt;. Now let's use &lt;code&gt;is&lt;/code&gt;&lt;/p&gt;
&lt;div class="codehilight code"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&amp;gt;&amp;gt;&amp;gt; a is b
True
&amp;gt;&amp;gt;&amp;gt; b is a
True
&amp;gt;&amp;gt;&amp;gt; a is c
False
&amp;gt;&amp;gt;&amp;gt; a is d
False
&amp;gt;&amp;gt;&amp;gt; b is c
False
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Here, the only &lt;code&gt;True&lt;/code&gt; statements are the comparison between &lt;code&gt;a&lt;/code&gt; and &lt;code&gt;b&lt;/code&gt;. This is because &lt;code&gt;b&lt;/code&gt; was initialized with the statement &lt;code&gt;b = a&lt;/code&gt;. The other two variables were initialized as their own statements and values. &lt;strong&gt;Remember, &lt;code&gt;is&lt;/code&gt; compares object references. If they are the same, it returned &lt;code&gt;True&lt;/code&gt;.&lt;/strong&gt;&lt;/p&gt;
&lt;div class="codehilight code"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&amp;gt;&amp;gt;&amp;gt; id(a), id(b), id(c), id(d)
(2267170738432, 2267170738432, 2267170545600, 2267170359040)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Since &lt;code&gt;a&lt;/code&gt; and &lt;code&gt;b&lt;/code&gt; are the same object, we get a &lt;code&gt;True&lt;/code&gt; on their comparison. The others are different, hence the &lt;code&gt;False&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id="nan-nan"&gt;nan == nan&lt;a class="headerlink" href="#nan-nan" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;nan&lt;/code&gt;, or Not a Number, is a floating point value that can not be converted to anything other than a float and is considered not equal to all other values. It's a common way to represent missing values in a data set.&lt;/p&gt;
&lt;p&gt;There is a key phrase in that description above that is the basis for this Gotcha:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;is considered not equal to all other values&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;It's common for software to check if two values are equal to one another prior to taking an action. For &lt;code&gt;nan&lt;/code&gt;, that does not work:&lt;/p&gt;
&lt;div class="codehilight code"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&amp;gt;&amp;gt;&amp;gt; float('nan') == float('nan')
False
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This prevents code like this from entering the &lt;code&gt;if&lt;/code&gt; block of an &lt;code&gt;if/else&lt;/code&gt; statement&lt;/p&gt;
&lt;div class="codehilight code"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;a&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;float&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'nan'&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;b&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;float&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'nan'&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;a&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;b&lt;/span&gt;:
...&lt;span class="w"&gt;   &lt;/span&gt;..&lt;span class="w"&gt; &lt;/span&gt;##&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;Do&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;something&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;equal&lt;/span&gt;
...&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;else&lt;/span&gt;:
...&lt;span class="w"&gt;   &lt;/span&gt;..&lt;span class="w"&gt; &lt;/span&gt;##&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;Do&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;something&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;not&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;equal&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;In this example, they are &lt;em&gt;never&lt;/em&gt; equal. &lt;/p&gt;
&lt;p&gt;This leads to an interesting, if not unintuitive, way of checking if a variable is a &lt;code&gt;nan&lt;/code&gt; value. Since &lt;code&gt;nan&lt;/code&gt; is not equal to &lt;em&gt;all&lt;/em&gt; other values, it is not equal to itself.&lt;/p&gt;
&lt;div class="codehilight code"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&amp;gt;&amp;gt;&amp;gt; a != a
True
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Like I said, "Interesting". But, when your code is looked at by others it's also "confusing". There is an easier way to show that you are checking for a &lt;code&gt;nan&lt;/code&gt; value. &lt;a href="https://docs.python.org/3/library/math.html#math.isnan"&gt;&lt;code&gt;isnan&lt;/code&gt;&lt;/a&gt;&lt;/p&gt;
&lt;div class="codehilight code"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;math&lt;/span&gt;
&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'nan'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;
&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'infinity'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;math&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isnan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kc"&gt;True&lt;/span&gt;
&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;math&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isnan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kc"&gt;False&lt;/span&gt;
&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;math&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isnan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kc"&gt;False&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;To me, that's a much clearer check that we want to see if the value is &lt;code&gt;nan&lt;/code&gt;. It's likely you aren't just passing &lt;code&gt;nan&lt;/code&gt; to a single variable. You're probably using a library like &lt;a href="https://numpy.org/"&gt;NumPy&lt;/a&gt; or &lt;a href="https://pandas.pydata.org/"&gt;Pandas&lt;/a&gt;. In that case, you have functions in each of those libraries that can check for &lt;code&gt;nan&lt;/code&gt; in a performant way.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;In NumPy the function has the same name but in the NumPy library: &lt;code&gt;numpy.isnan(value)&lt;/code&gt;. &lt;/li&gt;
&lt;li&gt;In Pandas, the function has a slightly different name: &lt;code&gt;pandas.isna(value)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="conclusion"&gt;Conclusion&lt;a class="headerlink" href="#conclusion" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Comparisons aren't always as straight forward as we'd like. I covered a few common comparison problems in Python here. &lt;/p&gt;
&lt;p&gt;Floating point comparisons are common across languages. Python has a few ways of making this easier for developers. I recommend utilizing &lt;code&gt;isclose&lt;/code&gt; as it keeps the code a bit cleaner and eliminates the need to wrap numbers in strings if using the &lt;code&gt;Decimal&lt;/code&gt; module.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;is&lt;/code&gt; should only be used to check if two items are referring to the same object. In any other case, it's not doing the check you want it to be doing. &lt;/p&gt;
&lt;p&gt;Lastly, &lt;code&gt;nan&lt;/code&gt; is equal to &lt;em&gt;nothing else&lt;/em&gt;. It's important to be aware of that before you start comparing values in your dataset to one another. &lt;/p&gt;</content><category term="Technical Solutions"/><category term="technical"/></entry></feed>