<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom"><title>Andrew Wegner | Ponderings of an Andy</title><link href="https://andrewwegner.com/" rel="alternate"/><link href="https://andrewwegner.com/feeds/all.atom.xml" rel="self"/><id>https://andrewwegner.com/</id><updated>2026-04-30T13:15:00-05:00</updated><subtitle>Can that be automated?</subtitle><entry><title>Management Debt: When Leadership Patterns Rot</title><link href="https://andrewwegner.com/management-debt-leadership-patterns-rot.html" rel="alternate"/><published>2026-04-30T13:15:00-05:00</published><updated>2026-04-30T13:15:00-05:00</updated><author><name>Andy Wegner</name></author><id>tag:andrewwegner.com,2026-04-30:/management-debt-leadership-patterns-rot.html</id><summary type="html">&lt;p&gt;Technical debt has a sibling that gets less airtime and, in my experience, does at least as much damage. Decision rights that drift, skip-levels that quietly fall off the calendar, review meetings that turn into status updates. Let's talk about how I clean them up once I notice they've gone bad.&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;Engineering leaders talk about &lt;a href="https://andrewwegner.com/tech-debt-management-strategic-approach.html"&gt;technical debt&lt;/a&gt; frequently. We track it, we visualize it, we plan around it, we explain it to executives. There's a related kind of debt that gets a fraction of that attention and, in my experience, does at least as much damage to engineering organizations. I've come to think of it as management debt - the engineering-leadership flavor of what &lt;a href="https://journals.plos.org/plosone/article?id=10.1371/journal.pone.0308183"&gt;recent research calls organizational debt&lt;/a&gt;: the accumulation of outdated structures, policies, and processes that quietly hinder how a company operates.&lt;/p&gt;
&lt;p&gt;By management debt I mean the gap between how leadership is supposed to operate and how it actually operates. Decision rights that have quietly migrated to whoever speaks up first. Skip-levels that became calendar holds nobody runs. Quarterly planning sessions that turned into backwards-looking status reviews. Performance feedback that gets saved up for the yearly review cycle because nobody had the harder conversation when a problem occurred.&lt;/p&gt;
&lt;p&gt;None of that looks like failure in the moment. It looks like a function nobody changed for three years. It still works, technically. Every change to it just costs more than it should.&lt;/p&gt;
&lt;h2 id="how-management-debt-accumulates"&gt;How management debt accumulates&lt;a class="headerlink" href="#how-management-debt-accumulates" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Most of the management debt I've seen didn't enter the organization through a bad decision. It entered through a deferred one. It entered through a "best at the time" decision that was never revisited.&lt;/p&gt;
&lt;p&gt;A skip-level got cancelled because a customer escalation came up. The next month it got cancelled again for similar reasons. After that it quietly fell off the calendar, "until things calm down." But, things never calm down. Six months later the engineering manager is making decisions that the director should be involved in, and the director doesn't know those decisions are being made.&lt;/p&gt;
&lt;p&gt;The same pattern shows up in design reviews. A team starts skipping the architecture review for "small" changes. Over time, the definition of "small" expands. A year later, the team is shipping huge changes without review and finding integration issues in production that the review process was specifically designed to catch.&lt;/p&gt;
&lt;p&gt;Each of these is defensible at the time. They look like sensible prioritization and efficiency, not debt. The problem is the same as with technical debt. The small concessions accumulate into a system that's worse than the one you designed, and at no single point did anyone make the call to make it worse.&lt;/p&gt;
&lt;h2 id="the-four-patterns-i-see-most"&gt;The four patterns I see most&lt;a class="headerlink" href="#the-four-patterns-i-see-most" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;I've come to recognize four common patterns. Each has its own diagnostic and its own fix.&lt;/p&gt;
&lt;h3 id="decision-rights-drift"&gt;Decision rights drift&lt;a class="headerlink" href="#decision-rights-drift" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;The clearest sign is that you can ask three people who's responsible for a particular call (production deploys, architectural decisions, prioritization of a bug over a feature) and get three different answers. The RACI chart says one thing. Practice has drifted somewhere else, usually because the formal owner delegated informally years ago and the delegation never got written down.&lt;/p&gt;
&lt;p&gt;The fix is uncomfortable. List the decisions that matter. Write down who actually owns each one today, not who's supposed to. Then write down who should own it. Where those two lists disagree, you've got a decision to make and to communicate.&lt;/p&gt;
&lt;p&gt;I've done this exercise on every leadership team. Every time, it surfaces things the leadership team didn't realize had drifted. &lt;a href="https://andrewwegner.com/scaling-teams-without-losing-culture.html"&gt;Fast-scaling teams&lt;/a&gt; are particularly prone to this. Decision rights set when the team was eight people rarely survive contact with thirty.&lt;/p&gt;
&lt;h3 id="ritual-decay"&gt;Ritual decay&lt;a class="headerlink" href="#ritual-decay" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Skip-levels, design reviews, retrospectives, planning sessions. These are leadership rituals that exist for a reason, and they decay along a predictable path. First the rhythm slips ("let's skip this week"). Then the agenda hollows out ("we don't really have anything to discuss, let's just do round-robin status"). Then the meeting becomes a calendar hold that nobody prepares for and nobody gets value from.&lt;/p&gt;
&lt;p&gt;The fix is to either kill the ritual or restore it to its original purpose. If you kill it, write down what it was supposed to do and confirm you're getting that signal another way. Keeping the calendar hold but letting it stay hollow is the worst of both worlds. People look at it, decide leadership doesn't take its own rituals seriously, and stop preparing for the ones that still matter.&lt;/p&gt;
&lt;h3 id="feedback-latency"&gt;Feedback latency&lt;a class="headerlink" href="#feedback-latency" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Healthy organizations give feedback close to whatever prompted it. &lt;a href="https://www.gallup.com/workplace/357764/fast-feedback-fuels-performance.aspx"&gt;Gallup's research&lt;/a&gt; is blunt about this: employees who get weekly feedback are far more likely to be engaged than those who only hear from their manager at year-end, and only 14% of employees say their performance review actually inspired them to improve. Organizations carrying management debt batch feedback into year-end performance reviews, where it lands as a list of surprises about events from eight months ago that nobody can do anything about.&lt;/p&gt;
&lt;p&gt;This one is the hardest to fix because the people involved have to do something they've been avoiding for a year. The structural piece is making sure feedback has a low-friction venue. Consistent 1:1s, written notes, performance check-ins quarterly rather than annually. The behavioral piece is the one that actually matters: the leader has to commit to delivering feedback soon after whatever prompted it, and their own manager has to check that it's happening.&lt;/p&gt;
&lt;p&gt;In my experience, this is where management debt does the most damage to retention. An engineer who hears "you should have been doing X differently for the past year" in a performance review either loses trust in their manager or starts interviewing. Often both. I've seen the same avoidance dynamic show up in &lt;a href="https://andrewwegner.com/management-failure-unlimited-pto.html"&gt;unlimited-PTO programs&lt;/a&gt; - a perk turns into a liability when the management work behind it never happens.&lt;/p&gt;
&lt;h3 id="meeting-metabolism"&gt;Meeting metabolism&lt;a class="headerlink" href="#meeting-metabolism" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;The slowest variant to spot is the gradual expansion of recurring meetings until calendar load makes deep work impossible for the leadership team itself. I like to call this "Calendar Tetris", and symptoms include frequent, overlapping meetings, often of a recurring variety. Each meeting was added for a defensible reason. The aggregate is that an engineering manager spends 32 hours a week in meetings and wonders why nothing strategic is getting done. On &lt;a href="https://andrewwegner.com/leading-distributed-teams-lessons.html"&gt;globally distributed teams&lt;/a&gt; the math gets worse, since synchronous slots compress into a few overlapping hours and "let's just add a recurring sync" becomes the default response to any coordination problem.&lt;/p&gt;
&lt;p&gt;The fix is blunt. Cancel everything recurring for two weeks. Re-add only the meetings that someone actively missed. The recurring meetings nobody noticed missing weren't serving the purpose they were created for. They were calendar inertia, and they were eating the leadership team's capacity. &lt;a href="https://www.worklife.news/culture/how-to-create-a-25-productivity-hike-lessons-from-shopifys-meetings-purge/"&gt;Shopify did exactly this at scale in early 2023&lt;/a&gt;, deleting roughly 12,000 recurring meetings in a single calendar purge with a two-week cooling-off period before anything could be re-added. Their reported outcome was a 33% drop in time spent in meetings and a 25% increase in completed projects.&lt;/p&gt;
&lt;p&gt;I've done this, and it's always refreshing when recurring meetings don't come back. Those weren't as needed as once believed.&lt;/p&gt;
&lt;h2 id="how-to-find-your-own-management-debt"&gt;How to find your own management debt&lt;a class="headerlink" href="#how-to-find-your-own-management-debt" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The diagnostic question I run on myself, and on every leadership team I've inherited, is this: What rituals are we running today that we wouldn't design from scratch if we were starting tomorrow? If the answer is "none," you're either not looking hard enough or you've genuinely just done a refactor. &lt;/p&gt;
&lt;p&gt;Another version: where in our leadership operating model are we relying on heroics or institutional memory instead of process? The answers are the line items in your management debt portfolio.&lt;/p&gt;
&lt;p&gt;A more concrete version I use with my staff: which decisions in the last quarter took longer than they should have, and why? The "why" almost always points to a piece of management debt. A missing decision right, a stale ritual, a communication channel that nobody runs anymore or that needs the help of management to clean up.&lt;/p&gt;
&lt;h2 id="why-this-matters-more-than-technical-debt"&gt;Why this matters more than technical debt&lt;a class="headerlink" href="#why-this-matters-more-than-technical-debt" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Technical debt slows code velocity. Management debt slows everything.&lt;/p&gt;
&lt;p&gt;These are decisions that aren't being made on time, feedback that isn't landing, and rituals that aren't producing signal. They show up as engineers who feel like they're working harder for less output, leaders who feel like they're spending the whole day in meetings without making progress, and a strategic plan that everyone agrees with and nobody actually executes.&lt;/p&gt;
&lt;p&gt;Engineering leaders are usually fluent in technical debt management. We track it, we explain its cost, we plan its repayment. We need the same discipline for the management practices themselves. Make it visible, talk about it explicitly, refactor it deliberately instead of waiting until the organization is genuinely broken.&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;Across the leadership roles I've held, the leaders I've seen succeed long-term aren't the ones with the most polished initial operating model. They're the ones who are willing to look at how they're actually leading, six or twelve months in, and fix what's drifted.&lt;/p&gt;
&lt;p&gt;Management debt is real. It accumulates the same way technical debt does. It costs at least as much, and it doesn't show up in any dashboard you already track. The next time you catch yourself thinking "we should really restart that ritual" or "we should really write down who owns that decision," that's not a nice-to-have on a backlog. That's overdue maintenance, and the longer you defer it the more expensive it gets.&lt;/p&gt;</content><category term="Leadership"/><category term="job"/><category term="leadership"/></entry><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>I Proved AI Could Beat Our Technical Interviews. Then I Had to Fix Them.</title><link href="https://andrewwegner.com/ai-broke-our-interview-process-i-had-to-fix-it.html" rel="alternate"/><published>2026-03-25T15:00:00-05:00</published><updated>2026-03-25T15:00:00-05:00</updated><author><name>Andy Wegner</name></author><id>tag:andrewwegner.com,2026-03-25:/ai-broke-our-interview-process-i-had-to-fix-it.html</id><summary type="html">&lt;p&gt;After proving AI could beat most technical assessments, I had to rebuild how I hire engineers. Here's the process I've developed; focused on what actually predicts success on the job.&lt;/p&gt;</summary><content type="html">
&lt;h2 id="the-interview-process-i-used-to-run"&gt;The interview process I used to run&lt;a class="headerlink" href="#the-interview-process-i-used-to-run" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;For years, I used timed online assessments as part of my hiring process. At PacketFabric, when we needed to scale the engineering team rapidly during our Series B, I helped evaluate several assessment platforms to find one that balanced candidate experience with the quality of signal we received as hiring managers. We landed on &lt;a href="https://www.woventeams.com/"&gt;Woven&lt;/a&gt;, and it worked. The assessments helped us move quickly through a high volume of candidates and gave us comparable data points across applicants.&lt;/p&gt;
&lt;p&gt;I had doubts, though. I've been opposed to LeetCode style interviews forever. Woven wasn't LeetCode as it provided amazing feedback to the candidate on what did and did not work. I liked the company so much that &lt;a href="https://andrewwegner.com/woven-client-to-woven-employee.html"&gt;I joined Woven&lt;/a&gt; for a while. But even with the best assessment platform I'd found, I saw cracks. The candidates who passed the assessments and joined the team didn't always succeed in the ways I expected. Some produced clean code under time pressure but struggled to collaborate with the team. Others passed technical screens convincingly but couldn't translate that skill into shipping features in a real codebase with real constraints. The assessment told me they could solve a problem in isolation. It told me very little about whether they could do the actual job.&lt;/p&gt;
&lt;p&gt;Then ChatGPT arrived, and my doubts became certainties.&lt;/p&gt;
&lt;p&gt;In late 2022 and early 2023, I ran ChatGPT through technical assessments on &lt;a href="https://andrewwegner.com/real-python-claude-code-live-course.html"&gt;LeetCode&lt;/a&gt;, &lt;a href="https://andrewwegner.com/breaking-the-interview-with-chatgpt.html"&gt;TestGorilla&lt;/a&gt;, &lt;a href="https://andrewwegner.com/chatgpt-breaks-more-interview-questions.html"&gt;CodeSignal&lt;/a&gt;, &lt;a href="https://andrewwegner.com/chatgpt-continues-beating-interview-questions.html"&gt;Codility&lt;/a&gt;, &lt;a href="https://andrewwegner.com/solving-more-interview-questions-with-chatgpt.html"&gt;HackerRank&lt;/a&gt;, and &lt;a href="https://andrewwegner.com/chatgpt-beats-more-interview-assessments.html"&gt;CoderByte&lt;/a&gt;. It passed most of them. Not marginally; it produced solutions that would have advanced through our pipeline. That wasn't a theoretical concern anymore. If a freely available tool could pass our technical assessment, we weren't evaluating engineering skill. We were evaluating test-taking ability, and we now had a machine that was better at test-taking than many humans. &lt;/p&gt;
&lt;p&gt;That was 2022 and 2023. We are now several AI model generations beyond that early ChatGPT. &lt;a href="https://andrewwegner.com/real-python-claude-code-live-course.html"&gt;Tools like Claude Code, Codex, Cursor, and GitHub Copilot are used by millions of developers daily&lt;/a&gt;. These aren't novelty chatbots any longer. They're integrated development environments that write, debug, and refactor production code. The gap between what AI could do when I ran those tests and what it can do today is enormous. Any assessment that was vulnerable to early ChatGPT is trivially solvable now.&lt;/p&gt;
&lt;p&gt;I had to rebuild how I hire.&lt;/p&gt;
&lt;h2 id="what-timed-assessments-and-take-homes-actually-measure"&gt;What timed assessments and take-homes actually measure&lt;a class="headerlink" href="#what-timed-assessments-and-take-homes-actually-measure" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Timed assessments reward speed and pattern recognition under artificial pressure. That correlates poorly with the actual work. Nobody ships production code in 45 minutes with a countdown timer and no access to documentation. The skills that make someone fast on a Codility problem like memorized algorithms, familiarity with specific puzzle patterns, comfort performing under observation, are not the same skills that make someone effective on a team building real products.&lt;/p&gt;
&lt;p&gt;Take-home projects have the opposite problem. Without time constraints or observation, there's no way to verify who actually did the work, how long it took, or what tools they used. Even before AI, take-homes had issues: they disproportionately disadvantage candidates with families or other time commitments, and they're difficult to evaluate consistently across candidates.&lt;/p&gt;
&lt;p&gt;Both formats share a deeper flaw though. They optimize for a narrow technical signal while telling you almost nothing about how a person thinks, communicates, or works with others. I've seen candidates pass assessments convincingly and then struggle on the job because they couldn't explain their decisions to a colleague, couldn't adapt when requirements changed, or couldn't take ownership of a system beyond the specific code they wrote. The assessment didn't predict any of that. It just told me they could solve a contrived problem under contrived conditions.&lt;/p&gt;
&lt;p&gt;I want to be fair to these tools, though. They feel rigorous and objective on the surface. That's why they're popular. The scores are comparable, the process is standardized, and it feels like you're making data-driven decisions. But the data is measuring the wrong thing.&lt;/p&gt;
&lt;h2 id="what-i-look-for-instead"&gt;What I look for instead&lt;a class="headerlink" href="#what-i-look-for-instead" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;My core philosophy has stayed the same across companies even though the specific steps change based on the team, the roles, and the company's needs. The philosophy is this: I want to understand how someone thinks, how they communicate, and whether they take genuine ownership of their work. The specific technology matters, but it matters less than those three things.&lt;/p&gt;
&lt;p&gt;How I evaluate depends on the level of the role.&lt;/p&gt;
&lt;h3 id="mid-level-and-senior-engineers"&gt;Mid-level and senior engineers&lt;a class="headerlink" href="#mid-level-and-senior-engineers" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;For experienced candidates, I focus on past project deep-dives. I ask the candidate to describe something they built - not a rehearsed elevator pitch, but a real system they worked on. Then I poke at it.&lt;/p&gt;
&lt;p&gt;The questions I ask depend on what the candidate brings up, and I rotate through several lines of inquiry based on what I'm hearing. If someone describes a system they architected, I'll ask about the tradeoffs they made and why. If they describe a project that clearly had rough patches, I'll ask what went wrong and dig into how they handled it. This isn't because "tell me about a failure" is a novel question, but because the follow-up conversation reveals depth. The most telling question, though, is usually some form of "what would you do differently if you built this again?" Candidates who were deeply involved in a project have learned something from it. They have opinions about what they'd change. Candidates who are exaggerating their involvement tend to stumble here because they haven't actually wrestled with the consequences of the original decisions.&lt;/p&gt;
&lt;p&gt;About half of the candidates I interview can genuinely defend their decisions in this format. The other half can describe what they built but can't articulate why they made the choices they did, or what they learned from the experience. That's the signal. I'm not looking for perfect decisions. I'm looking for evidence that they understand systems deeply enough to reason about them, not just implement them.&lt;/p&gt;
&lt;p&gt;For more senior and architecturally-focused roles, I lean harder on design tradeoff discussions. For mid-level engineers, I focus more on the "what went wrong" and "what would you change" angles, which are more accessible and still reveal a lot.&lt;/p&gt;
&lt;h3 id="junior-engineers"&gt;Junior engineers&lt;a class="headerlink" href="#junior-engineers" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Past project deep-dives work less well for junior candidates. They don't have enough experience to defend decisions they haven't had the opportunity to make yet. Asking a recent graduate to explain the architectural tradeoffs of their senior capstone project isn't a fair evaluation.&lt;/p&gt;
&lt;p&gt;Instead, I use a small live coding exercise. Something that can be written in 5-10 lines of code, with multiple valid approaches. For example, a problem that can be solved iteratively and recursively. I ask them to write it one way and we talk through it for a minute or two. Then I ask them to write it the other way. I don't tell them in advance that I'll ask for the alternative approach. I want to see what they do naturally when the problem shifts.&lt;/p&gt;
&lt;p&gt;The code itself is almost secondary. What I'm evaluating is the conversation. Can they reason about the difference between the two approaches? Can they articulate why one might be preferable in a given context? Do they get flustered when asked to think differently, or do they engage with the challenge? A junior engineer who writes imperfect code but can reason clearly about alternatives and ask good questions is far more promising than one who produces a clean solution but can't explain their thinking.&lt;/p&gt;
&lt;h3 id="leads-and-directors"&gt;Leads and directors&lt;a class="headerlink" href="#leads-and-directors" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;For leadership roles, I shift to scenario and system design conversations. Less about code, more about organizational and architectural judgment. How would you structure a team to build a particular kind of system? How would you evaluate and prioritize a backlog of technical debt? How do you handle a situation where product and engineering disagree on timeline or scope? The signal here is similar to the engineering deep-dives. I'm listening for whether a candidate can reason through ambiguity, make a decision with incomplete information, and explain their thinking clearly. A strong candidate will ask clarifying questions, acknowledge tradeoffs, and arrive at a defensible position. A weak candidate will give a textbook answer that doesn't account for the messy reality of the scenario.&lt;/p&gt;
&lt;h3 id="technical-product-managers"&gt;Technical product managers&lt;a class="headerlink" href="#technical-product-managers" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;I also interview Technical Product Managers (TPM), and the approach shares the same philosophy as the engineering deep-dives. I'll present a feature or problem that has already been solved so that I have detailed knowledge of what has worked. I ask them to walk through how they'd approach it. I'm watching how they think through the problem, what questions they ask, what tradeoffs they identify, and how they communicate their reasoning. Then during the conversation, I poke at their decisions the same way I would if I were working through the problem with a TPM on my team. Can they explain why they prioritized one approach over another? Can they articulate the user impact? Can they adjust their thinking when I introduce a constraint they hadn't considered? This fits directly into the communication signal I look for across every role which is the ability to reason through a problem, defend your position, and adapt when new information arrives.&lt;/p&gt;
&lt;h2 id="the-soft-skills-that-predict-long-term-success"&gt;The soft skills that predict long-term success&lt;a class="headerlink" href="#the-soft-skills-that-predict-long-term-success" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Technical skill gets someone through the interview. Soft skills determine whether they succeed on the team. I assess these through a combination of specific questions, observing behavior throughout the interview, and paying close attention to how candidates describe the systems and processes they've worked in.&lt;/p&gt;
&lt;p&gt;Three signals have been the strongest predictors across my career:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Ownership without ego.&lt;/strong&gt; I want someone who takes responsibility for the system, not just the lines of code they wrote. But ownership can become toxic when it turns into "this is my baby and it's always right and the user is wrong." The best engineers own the outcome for the user, not just the implementation. When something breaks, they don't deflect. But they also don't get defensive when someone suggests a different approach or when a user reports that the system isn't working the way they intended. You can often hear the difference in how candidates describe past work: do they talk about what "I built" in isolation, or do they talk about how the system served its users and how the team improved it over time?&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Communication.&lt;/strong&gt; Can they explain a technical decision to someone who isn't an engineer? Can they disagree with a colleague constructively? The interview itself is a communication exercise and I'm paying attention to how clearly they explain their past projects, how they handle follow-up questions, and whether they can adjust their level of detail based on the conversation. An engineer who can only communicate with other engineers who share their context will hit a ceiling quickly, regardless of their technical ability.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Curiosity and willingness to learn.&lt;/strong&gt; The technology stack will change. The industry is already changing and AI has introduced so much disruption in such a short time that it is vital candidates are willing to learn and adapt. I've worked across many industries in my career, and the engineers who thrived through transitions were the ones who were genuinely energized by learning something new rather than threatened by it. In interviews, I listen for whether candidates describe their learning in terms of genuine interest or obligation. "I had to learn Kubernetes for the role" sounds very different from "I got interested in how our deployment pipeline could be more reliable, which led me to Kubernetes."&lt;/p&gt;
&lt;h2 id="designing-interviews-that-ai-cant-shortcut"&gt;Designing interviews that AI can't shortcut&lt;a class="headerlink" href="#designing-interviews-that-ai-cant-shortcut" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The ChatGPT experiment wasn't just a blog series - it fundamentally changed how I approach hiring. My current process is designed so that AI assistance is either irrelevant or immediately visible.&lt;/p&gt;
&lt;p&gt;You can't have ChatGPT defend your past project decisions in a live conversation. When I ask a candidate what they'd do differently about a system they built two years ago, there's no prompt that generates an authentic answer. The deep-dive format is inherently AI-resistant because it's evaluating lived experience, not producible knowledge.&lt;/p&gt;
&lt;p&gt;For the junior coding exercises, the code portion could theoretically be AI-assisted. But the follow-up conversation, "now write it the other way, and explain why you'd choose one approach over the other", reveals immediately whether the candidate understands what they wrote. I've caught candidates who clearly used AI for their solution and then could not explain their own code when I asked follow-up questions. The conversation is the evaluation, not the code.&lt;/p&gt;
&lt;p&gt;My position on AI use in interviews is nuanced. I don't ban AI tools, but I do expect candidates to disclose if they're using them. If a candidate uses AI and tells me about it, we can have a productive conversation about how they used it and what their own contribution was. If we've asked, and they haven't disclosed, that's a failure of integrity that outweighs technical competence. When you're hiring someone to join a team, trust matters as much as skill.&lt;/p&gt;
&lt;p&gt;This isn't a hypothetical concern. I've encountered candidates who were clearly using AI during interviews and couldn't explain their own answers. I'm not alone. &lt;a href="https://www.cnbc.com/2025/03/09/google-ai-interview-coder-cheat.html"&gt;CNBC reported in 2025&lt;/a&gt; that tools like Interview Coder and Cluely now use invisible screen overlays that are undetectable by standard screen sharing, and hiring managers describe the telltale pattern of a pause, an "Hmm," and then a suspiciously perfect answer. An &lt;a href="https://www.fabrichq.ai/blogs/state-of-ai-interview-cheating-in-2026-insights-from-19-368-interviews"&gt;analysis of over 19,000 interviews by Fabric&lt;/a&gt; found that cheating adoption more than doubled from 15% to 35% between June and December 2025, with technical roles showing a 48% cheating rate.&lt;/p&gt;
&lt;p&gt;I've also encountered the broader trend of candidates working at multiple companies simultaneously. This is a &lt;a href="https://fortune.com/2025/08/03/workers-holding-multiple-full-time-jobs-secretly-remote-work-40-hour-week-overemployment-high-income/"&gt;real and growing problem in remote engineering&lt;/a&gt;. A &lt;a href="https://www.resumebuilder.com/7-in-10-remote-workers-have-multiple-jobs/"&gt;ResumeBuilder.com survey&lt;/a&gt; found that 37% of remote workers hold two full-time jobs, with AI tools making it increasingly feasible to juggle multiple roles within a standard workweek. Both of these trends make it even more critical that your interview process evaluates the actual human sitting across from you, not just the output they produce.&lt;/p&gt;
&lt;h2 id="what-i-havent-solved-yet"&gt;What I haven't solved yet&lt;a class="headerlink" href="#what-i-havent-solved-yet" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;No hiring process is perfect, and I don't want to pretend mine is. There are areas I'm still actively working to improve.&lt;/p&gt;
&lt;p&gt;Evaluating candidates from very different technology stacks remains a challenge. When someone's entire career has been in a language or ecosystem that's different from what we use, the past project deep-dive still works for assessing how they think, but it's harder to gauge how quickly they'll become productive in a new environment. I haven't found a clean solution for this that doesn't fall back on the kind of generalized assessments I've moved away from.&lt;/p&gt;
&lt;p&gt;Scaling the process is another challenge. Deep-dive conversations take time and they're significantly more time-intensive per candidate than a timed assessment that can be administered asynchronously. When you're hiring for many roles simultaneously, the time cost is real. I've mitigated this by involving more of the team in interviews, but that creates its own overhead.&lt;/p&gt;
&lt;p&gt;And the overall hiring pipeline is still slower than I'd like. The industry-wide process of sourcing, screening, interviewing, and closing candidates has friction at every stage. Improving my interview process doesn't fix the weeks lost to scheduling coordination, offer negotiations, and notice periods.&lt;/p&gt;
&lt;h2 id="what-actually-matters"&gt;What actually matters&lt;a class="headerlink" href="#what-actually-matters" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;I've been hiring engineers across industries for nearly two decades and for companies ranging from pre-seed startups to Fortune 500 corporations. I've been wrong about candidates in both directions; people I was confident about who didn't work out, and people I had reservations about who became some of the strongest members of the team.&lt;/p&gt;
&lt;p&gt;What I've learned is that the interview process should evaluate what actually matters on the job: Can this person reason about complex systems? Can they communicate clearly and work constructively with others? Do they take ownership of outcomes, not just code? Are they honest about what they know and what they don't?&lt;/p&gt;
&lt;p&gt;No timed assessment answers those questions. A conversation does.&lt;/p&gt;</content><category term="Leadership"/><category term="job"/><category term="leadership"/><category term="ai-interviews"/></entry><entry><title>The Remote Engineering Leadership Playbook: Why Distance Doesn't Diminish Performance</title><link href="https://andrewwegner.com/why-remote-work-is-good-for-your-team.html" rel="alternate"/><published>2025-08-22T09:00:00-05:00</published><updated>2025-08-22T09:00:00-05:00</updated><author><name>Andy Wegner</name></author><id>tag:andrewwegner.com,2025-08-22:/why-remote-work-is-good-for-your-team.html</id><summary type="html">&lt;p&gt;Proven processes transform distributed teams from liability into competitive advantage. This talks about why I think that is and how to ensure your team is successful when they are remote.&lt;/p&gt;</summary><content type="html">
&lt;h2 id="remote-engineering-works-when-done-right"&gt;Remote Engineering Works When Done Right&lt;a class="headerlink" href="#remote-engineering-works-when-done-right" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;When the &lt;a href="https://www.bls.gov/productivity/notices/2024/productivity-and-remote-work.htm"&gt;U.S. Bureau of Labor Statistics&lt;/a&gt; released its comprehensive analysis of remote work productivity in 2024, something stood out. A one percentage-point increase in the percentage of remote workers is associated with a 0.08 percentage-point increase in total factor productivity. This covered over 60 industries from 2019-2023 (pre and post pandemic).&lt;/p&gt;
&lt;p&gt;Rephrasing that slightly, as the number of remote workers increase at a company, the total productivity increases at the company.&lt;/p&gt;
&lt;p&gt;For engineering leaders, this validates what many of us have seen firsthand. Remote engineering teams can outperform their co-located counterparts &lt;em&gt;when proper processes exist&lt;/em&gt;. The difference between success and struggle lies in having the right systems in place.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://andrewwegner.com/leading-distributed-teams-lessons.html"&gt;After scaling multiple engineering teams, I've learned that distance becomes a liability only when we lack intentional systems&lt;/a&gt;. The companies thriving with remote engineering teams have built deliberate frameworks for collaboration across timezones, asynchronous communication, and meaningful ways of measuring the team's results.&lt;/p&gt;
&lt;h2 id="remote-engineering-success"&gt;Remote Engineering Success&lt;a class="headerlink" href="#remote-engineering-success" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;h3 id="cross-timezone-collaboration-that-actually-works"&gt;Cross-Timezone Collaboration That Actually Works&lt;a class="headerlink" href="#cross-timezone-collaboration-that-actually-works" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;The biggest mistake I see engineering leaders make is treating timezone differences as a scheduling problem when they should approach it as a workflow design challenge. With &lt;a href="https://www.itpro.com/software/development/software-engineer-remote-work-trends-rto"&gt;80% of software engineers working at least partially remote by the end of 2025&lt;/a&gt;, timezone optimization becomes a core competency for leaders.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The Follow-the-Sun Development Model&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Instead of forcing everyone into the same working hours, successful teams design workflows around natural handoffs. The key is documentation driven handoffs. Each transition requires clear context about what was accomplished, explicit next steps and acceptance criteria, identification of blockers and dependencies, and quality gates that prevent broken work from moving forward.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Strategic Overlap Windows&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;While asynchronous work drives the majority of productivity, you still need dedicated synchronous time for problem-solving and relationship building. I've found that scheduling dedicated overlapping windows a couple times per week provides face-to-face interaction without constraining individual productivity. Teams need &lt;em&gt;at least&lt;/em&gt; one session that focuses on technical discussions and a second that builds team culture. Be intentional about these meetings though. Too many and you fall into the trap of making this a scheduling problem. Too few, and your team starts siloing itself.&lt;/p&gt;
&lt;p&gt;Remote teams need fewer but more intentional synchronous touchpoints instead of trying to replicate in-office meeting cadences.&lt;/p&gt;
&lt;h3 id="asynchronous-communication-as-a-strategic-advantage"&gt;Asynchronous Communication as a Strategic Advantage&lt;a class="headerlink" href="#asynchronous-communication-as-a-strategic-advantage" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;&lt;a href="https://about.gitlab.com/blog/2020/08/27/measuring-engineering-productivity-at-gitlab/"&gt;GitLab's engineering team&lt;/a&gt; scaled from 100 to 280 engineers in 1.5 years while remaining fully remote in the early 2020s. They deliberately avoided making their primary productivity metric, Merge Request Rate, an individual metric because they wanted to encourage collaborative behavior rather than siloed competition.&lt;/p&gt;
&lt;p&gt;The takeaway from this, to me, is that successful remote teams optimize for collective intelligence rather than individual heroics.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Documentation-First Decision Making&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Every architectural decision, technical specification, and process change should be documented before it's discussed. Writing forces precision in thinking while enabling asynchronous input from team members across timezones. This approach creates institutional memory that survives role changes and allows ideas to be evaluated on merit rather than presentation skills.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Structured Communication Protocols&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Remote teams need explicit protocols for different types of information. Urgent blockers require immediate Slack notifications with clear context. Technical discussions benefit from a structured request for comments process. Status updates should follow standardized formats that enable quick scanning. Relationship building happens through dedicated informal channels and virtual chats.&lt;/p&gt;
&lt;p&gt;The goal is more effective communication that respects individual deep work time while enabling collective progress, instead of simply increasing communication volume.&lt;/p&gt;
&lt;h3 id="meaningful-metrics-that-drive-performance"&gt;Meaningful Metrics That Drive Performance&lt;a class="headerlink" href="#meaningful-metrics-that-drive-performance" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Most remote engineering initiatives fail because they measure activity instead of impact. The &lt;a href="https://dora.dev/"&gt;DORA metrics&lt;/a&gt; provide a research-backed framework for measuring what actually matters.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Deployment Frequency&lt;/strong&gt; measures how often your team ships code to production. &lt;strong&gt;Lead Time for Changes&lt;/strong&gt; tracks the time from commit to deployment. &lt;strong&gt;Mean Time to Recovery&lt;/strong&gt; shows how quickly you recover from production issues. &lt;strong&gt;Change Failure Rate&lt;/strong&gt; calculates the percentage of deployments that cause problems.&lt;/p&gt;
&lt;p&gt;These metrics work particularly well for remote teams because they focus on outcomes rather than presence. A team that deploys multiple times per day with low failure rates is performing well, regardless of whether they're working from an office or their kitchen table.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Team-Level Indicators That Matter&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Beyond DORA metrics, successful remote engineering teams track cross-team collaboration frequency to prevent silos, knowledge sharing effectiveness measured by new team member productivity timelines, sprint predictability showing whether teams can reliably estimate and deliver, and technical debt management indicating whether the team builds for today or tomorrow.&lt;/p&gt;
&lt;p&gt;The key is making these metrics visible and actionable. GitLab makes their &lt;a href="https://about.gitlab.com/handbook/engineering/development/performance-indicators/"&gt;engineering productivity metrics publicly available&lt;/a&gt;, creating accountability and continuous improvement.&lt;/p&gt;
&lt;h2 id="culture-at-scale"&gt;Culture at Scale&lt;a class="headerlink" href="#culture-at-scale" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Technology and processes enable remote work, but culture makes it sustainable. The most successful distributed engineering teams I've worked with share common cultural characteristics.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Ownership Over Oversight&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Remote work amplifies individual accountability. When you can't rely on hallway conversations and office presence, team members must take genuine ownership of their commitments. This requires hiring for self-direction and creating systems that support autonomous decision-making.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Mentorship Through Documentation&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://andrewwegner.com/junior-engineer-crisis-ai-code-generation.html"&gt;Traditional mentorship&lt;/a&gt; happens as junior developers overhear senior conversations and absorb knowledge informally. Remote teams must be intentional about knowledge transfer through code reviews, architectural decision records, and documented retrospectives.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Celebration and Recognition&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Remote teams need to work harder at celebrating wins and recognizing contributions. Without spontaneous high-fives and impromptu team lunches, recognition becomes a deliberate practice that requires systems and consistency.&lt;/p&gt;
&lt;h2 id="the-competitive-advantage-of-getting-this-right"&gt;The Competitive Advantage of Getting This Right&lt;a class="headerlink" href="#the-competitive-advantage-of-getting-this-right" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The &lt;a href="https://www.bls.gov/productivity/notices/2024/productivity-and-remote-work.htm"&gt;Bureau of Labor Statistics data&lt;/a&gt; reveals something else too. A 1 percentage-point increase in the percentage of remote workers is associated with a 0.4 percentage-point decrease in growth in unit office building costs. This extends beyond productivity to sustainable economics, and makes me raise an eyebrow at the number of return of office mandates we have been seeing.&lt;/p&gt;
&lt;p&gt;Companies that master remote engineering practices can access talent from anywhere rather than just expensive tech hubs, reduce operational overhead without sacrificing performance, build more resilient teams that aren't dependent on physical proximity, and create competitive advantages through superior asynchronous collaboration.&lt;/p&gt;
&lt;p&gt;The question has shifted from whether remote engineering teams can work to whether your organization will develop the processes and culture to unlock this advantage. Many companies continue treating remote work as a necessary accommodation rather than recognizing it as a strategic opportunity.&lt;/p&gt;
&lt;p&gt;Remote engineering success requires intentional design rather than hoping things work out naturally. For leaders willing to invest in the right frameworks, the returns in productivity, talent access, and competitive positioning are substantial and measurable.&lt;/p&gt;</content><category term="Leadership"/><category term="job"/><category term="leadership"/></entry><entry><title>Junior Engineer Crisis: How AI Code Generation is Reshaping Engineering Teams</title><link href="https://andrewwegner.com/junior-engineer-crisis-ai-code-generation.html" rel="alternate"/><published>2025-08-18T09:00:00-05:00</published><updated>2025-08-18T09:00:00-05:00</updated><author><name>Andy Wegner</name></author><id>tag:andrewwegner.com,2025-08-18:/junior-engineer-crisis-ai-code-generation.html</id><summary type="html">&lt;p&gt;An analysis of how artificial intelligence is transforming software development careers, team structures, and how engineers build expertise.&lt;/p&gt;</summary><content type="html">
&lt;h2 id="the-data-that-changes-everything"&gt;The Data That Changes Everything&lt;a class="headerlink" href="#the-data-that-changes-everything" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The software development industry is experiencing a shift that's happening faster than most organizations realize. Three statistics captured my attention this year, and collectively, they paint a picture of an industry in transition:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;AI now generates 41% of all code, with &lt;a href="https://itsconchur.substack.com/p/41-of-code-is-now-ai-generated-should"&gt;256 billion lines written in 2024 alone&lt;/a&gt;, according to Stability AI CEO Emad Mostaque.&lt;/li&gt;
&lt;li&gt;Software engineer job listings are down 35% from five years ago, according to &lt;a href="https://blog.pragmaticengineer.com/software-engineer-jobs-five-year-low/"&gt;The Pragmatic Engineer's analysis&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;AI tools make experienced, open source, software engineers 19% slower, not faster, based on a &lt;a href="https://metr.org/blog/2025-07-10-early-2025-ai-experienced-os-dev-study/"&gt;randomized controlled trial by METR Research&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That last statistic flies in the face of all stories we're seeing at this point in 2025. Through my time scaling engineering teams, I've learned to pay attention when data contradicts conventional wisdom. If AI is making experienced engineers slower, what does that mean for how we think about productivity, learning, and career development in software engineering?&lt;/p&gt;
&lt;p&gt;The implications extend far beyond individual productivity. They touch the core of how engineering teams function, how knowledge transfers between generations of software engineers, and ultimately, how we build the technical leaders of tomorrow.&lt;/p&gt;
&lt;h2 id="the-productivity-paradox"&gt;The Productivity Paradox&lt;a class="headerlink" href="#the-productivity-paradox" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The conventional narrative around AI in software development has focused on speed and efficiency. &lt;a href="https://hatchworks.com/blog/gen-ai/generative-ai-statistics/"&gt;AI improves employee productivity by up to 66% across various roles&lt;/a&gt;, according to recent studies. Yet when it comes to experienced software engineers, the opposite appears to be true.&lt;/p&gt;
&lt;p&gt;The METR Research study revealed that when experienced engineers used AI tools, they took longer to complete tasks compared to working without AI assistance. This finding challenges the central assumption driving AI adoption in engineering teams.&lt;/p&gt;
&lt;h3 id="why-does-this-happen"&gt;Why does this happen?&lt;a class="headerlink" href="#why-does-this-happen" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;In my experience, software development isn't just about code generation. It's about problem-solving, architecture decisions, and understanding complex system interactions. When software engineers rely on AI to generate code, they often spend additional time not doing these. Instead they are reviewing and understanding what the AI spit out to ensure it aligns with system requirements. Then they spend time debugging the code due to missed requirements, edge cases, or lack of context provided to the coding assistant.&lt;/p&gt;
&lt;p&gt;This productivity paradox reveals something crucial. The value of software development isn't in typing speed. It's in thinking speed.&lt;/p&gt;
&lt;h3 id="reality-check"&gt;Reality Check&lt;a class="headerlink" href="#reality-check" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Meanwhile, &lt;a href="https://www.mckinsey.com/capabilities/mckinsey-digital/our-insights/superagency-in-the-workplace-empowering-people-to-unlock-ais-full-potential-at-work"&gt;almost all companies are investing in AI, but just 1% believe they are at maturity&lt;/a&gt;, according to McKinsey's 2025 workplace analysis. This suggests that even organizations themselves are struggling to effectively integrate AI tools into their development processes.&lt;/p&gt;
&lt;p&gt;The disconnect between AI's promise and its current reality in software development creates a unique opportunity for engineering leaders who understand how to navigate this transition thoughtfully.&lt;/p&gt;
&lt;h2 id="the-mentorship-gap"&gt;The Mentorship Gap&lt;a class="headerlink" href="#the-mentorship-gap" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Traditional software engineering career progression has followed a predictable pattern:&lt;/p&gt;
&lt;p&gt;Junior Engineer -&amp;gt; Mid-level Engineer -&amp;gt; Senior Engineer -&amp;gt; Technical Lead&lt;/p&gt;
&lt;p&gt;This progression relied heavily on mentorship, code reviews, and the gradual transfer of knowledge from experienced engineers to new team members. AI is disrupting this learning pipeline in ways we're just beginning to understand.&lt;/p&gt;
&lt;h3 id="the-traditional-knowledge-transfer-model"&gt;The Traditional Knowledge Transfer Model&lt;a class="headerlink" href="#the-traditional-knowledge-transfer-model" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Historically, junior engineers learned through writing code that was reviewed by senior engineers. They also participated in pair programming and code reviews that taught best practices and patterns, and working on increasingly complex tasks. Crucially, they also debugged their own mistakes and &lt;em&gt;learned from failure&lt;/em&gt;. Generative AI deprives the youngest team members of this skill because they are debugging code that was generated for them rather than by them. They don't have the depth of knowledge over this code snippet. They haven't spent hours working to solve a problem in &lt;em&gt;this one line of code&lt;/em&gt;. The failure of the code is not their own, it's the AI's.&lt;/p&gt;
&lt;p&gt;Each of these steps built not just coding skills, but engineering judgment to teach them the ability to make good technical decisions under uncertainty.&lt;/p&gt;
&lt;h3 id="the-ai-disrupted-model"&gt;The AI-Disrupted Model&lt;a class="headerlink" href="#the-ai-disrupted-model" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Now, increasingly, the model is that the AI generates code based on natural language specifications. Then a senior, not junior, engineer reviews the output. As the debugging loop starts, the focus is on the AI prompt engineering instead of the code. Code reviews become a validation of the AI and aren't teaching moments any longer. Junior engineers solve less problems and babysit the AI instead.&lt;/p&gt;
&lt;p&gt;This shift significantly changes the nature of mentorship on engineering teams.&lt;/p&gt;
&lt;h3 id="the-hidden-cost"&gt;The Hidden Cost&lt;a class="headerlink" href="#the-hidden-cost" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;When senior engineers spend their time reviewing AI-generated code instead of mentoring junior engineers, we lose the critical element of human knowledge transfer.&lt;/p&gt;
&lt;p&gt;A senior engineer reviewing a junior's code can ask questions like:
- "Why did you choose this approach?"
- "What other options did you consider?"
- "How would this handle increased load?"
- "What happens if this service is unavailable?"&lt;/p&gt;
&lt;p&gt;These questions build engineering judgment. But when reviewing AI-generated code, the questions become:
- "Did the AI choose the right pattern?"
- "Does this handle our edge cases?"
- "Is this consistent with our architecture?"&lt;/p&gt;
&lt;p&gt;The learning opportunity shifts from building problem-solving skills to validating AI decisions.&lt;/p&gt;
&lt;h2 id="what-were-really-losing"&gt;What We're Really Losing&lt;a class="headerlink" href="#what-were-really-losing" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The impact of AI on junior engineering roles goes beyond individual career paths. We're potentially losing institutional knowledge, problem-solving capabilities, and the human intuition that comes from learning to code through struggle and iteration.&lt;/p&gt;
&lt;h3 id="problem-decomposition-skills"&gt;Problem Decomposition Skills&lt;a class="headerlink" href="#problem-decomposition-skills" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;One of the most valuable skills a software engineer develops is the ability to break complex problems into smaller, manageable pieces. This skill typically develops through experience. This is through encountering problems that are too big to solve all at once and learning to approach them systematically.&lt;/p&gt;
&lt;p&gt;When AI handles this decomposition automatically, junior engineers don't develop this critical thinking muscle. They become skilled at describing problems to AI rather than solving them independently.&lt;/p&gt;
&lt;h3 id="debugging-intuition"&gt;Debugging Intuition&lt;a class="headerlink" href="#debugging-intuition" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Experienced engineers often talk about having a "gut feeling" about where bugs might be hiding or what might cause a system to fail under load. This intuition develops through years of debugging their own mistakes and understanding how systems fail in practice.&lt;/p&gt;
&lt;p&gt;AI-generated code fails differently than human-written code. It might be syntactically correct but miss business logic edge cases. It might follow patterns perfectly but make assumptions about data that don't hold in production. Learning to debug AI code is a different skill from learning to debug human reasoning errors.&lt;/p&gt;
&lt;h3 id="architectural-thinking"&gt;Architectural Thinking&lt;a class="headerlink" href="#architectural-thinking" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Understanding why certain architectural patterns exist, when to apply them, and how they impact system behavior requires experience with the consequences of different choices. This understanding traditionally developed through making mistakes, seeing systems break, and learning from the aftermath.&lt;/p&gt;
&lt;p&gt;When AI makes many of these architectural decisions automatically, junior engineers may learn to recognize good patterns without understanding why they're good or when they might be inappropriate.&lt;/p&gt;
&lt;h3 id="the-compound-effect"&gt;The Compound Effect&lt;a class="headerlink" href="#the-compound-effect" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Perhaps most concerning is the compound effect of these changes. If junior engineers don't develop problem-solving skills, debugging intuition, and architectural thinking, who becomes our next generation of senior engineers?&lt;/p&gt;
&lt;p&gt;&lt;a href="https://dev.to/itamartati/why-software-engineers-are-finding-it-harder-to-get-a-job-in-2025-the-changing-standards-of-hiring-19po"&gt;Software engineers are finding it harder to get jobs in 2025 due to changing hiring standards&lt;/a&gt;, according to analysis from the software engineering community. The bar for what constitutes "junior engineer" skills is rising, but the pathways to develop those skills are being disrupted by AI.&lt;/p&gt;
&lt;h2 id="the-strategic-response"&gt;The Strategic Response&lt;a class="headerlink" href="#the-strategic-response" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The organizations that thrive in this transition won't be the ones that embrace or reject AI wholesale. They'll be the ones that thoughtfully integrate AI while preserving the human elements that create strong engineering teams.&lt;/p&gt;
&lt;h3 id="understanding-the-market-reality"&gt;Understanding the Market Reality&lt;a class="headerlink" href="#understanding-the-market-reality" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Despite these challenges, &lt;a href="https://www.turing.com/blog/software-development-statistics"&gt;employment opportunities for software engineers are still expected to grow by 20%&lt;/a&gt;, according to recent market analysis. This suggests that demand for engineering talent remains strong, but the nature of that talent is evolving.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://www.mckinsey.com/industries/technology-media-and-telecommunications/our-insights/how-an-ai-enabled-software-product-development-life-cycle-will-fuel-innovation"&gt;McKinsey's analysis indicates that AI has the potential to fundamentally transform software development processes&lt;/a&gt;, but successful transformation requires deliberate strategy, not just tool adoption. This point bears repeating because it's vital for leaders to understand.&lt;/p&gt;
&lt;div class="admonition attention"&gt;
&lt;p class="admonition-title"&gt;Attention&lt;/p&gt;
&lt;p&gt;Successful transformation requires deliberate strategy, not just tool adoption&lt;/p&gt;
&lt;/div&gt;
&lt;p&gt;Bringing Copilot, Cursor, Claude Code or other AI coding assistants to your team does not guarantee success. It is &lt;em&gt;a&lt;/em&gt; step in being successful in the AI transformation. It is not &lt;em&gt;the&lt;/em&gt; step.&lt;/p&gt;
&lt;h3 id="three-strategic-approaches"&gt;Three Strategic Approaches&lt;a class="headerlink" href="#three-strategic-approaches" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Based on my experience scaling engineering teams and observing successful AI integration, three strategic approaches are emerging.&lt;/p&gt;
&lt;p&gt;The first approach is redefining what it means to be a "Junior Engineer." Instead of eliminating junior positions, successful organizations are redefining what junior software engineers need to be good at. Traditionally, a junior engineer would be able to write syntactically correct code, follow team development patterns, understand the language and framework used by the team to a degree, and be able to implement requirements that are well defined.&lt;/p&gt;
&lt;p&gt;However, a new AI-era junior engineer needs different skills. They need to be able to analyze and decompose a problem, showing system thinking and architecture understanding. This enables them to collaborate with AI coding assistants to generate solutions that actually solve the problem. Critically, they can perform a review of this output.&lt;/p&gt;
&lt;p&gt;The most effective teams I've observed use AI as a teaching tool rather than a replacement for learning. Junior software engineers work with both AI tools and senior engineers, using AI to handle boilerplate while focusing human mentorship on architectural decisions and problem-solving approaches. This keeps several of the mentorship gaps minimal and builds a foundation that today's junior engineers can build on as they grow in their careers.&lt;/p&gt;
&lt;p&gt;I've also heard of teams implementing regular "AI-free" coding sessions for engineers to ensure they can solve problems without the assistance of the AI tooling to build their early troubleshooting and debugging muscles. This type of thing also has been extended to code reviews, where engineers must be able to explain why the AI tool took certain approaches and if there are alternatives that might have been better.&lt;/p&gt;
&lt;p&gt;Personally, these approaches feel somewhat academic. A junior engineer is still a professional not a student in school. I think there could be good training around these ideas, but I don't think it should feel like we are taking away a tool. Instead, teach how to use it correctly while filling in the knowledge gaps.&lt;/p&gt;
&lt;p&gt;In my experience, successful teams are making knowledge transfer deliberate through recording architectural decisions that were made and what alternatives were considered. Through troubleshooting sessions, problem solving sessions, and "pair support" to dive into complex system problems.&lt;/p&gt;
&lt;p&gt;Mentoring from anyone to anyone is important too. Everyone on the team has something to contribute and teach. Whether it's complex system architecture that a senior engineer shares with a more junior team member or having a junior engineer lead a session on how they solved a problem. All of this is important for the whole team.&lt;/p&gt;
&lt;h2 id="redefining-junior-engineer-value"&gt;Redefining Junior Engineer Value&lt;a class="headerlink" href="#redefining-junior-engineer-value" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The key insight is that AI doesn't eliminate the need for junior developers. It changes what makes junior software engineers valuable, though.&lt;/p&gt;
&lt;h3 id="code-generation-to-code-curation"&gt;Code Generation to Code Curation&lt;a class="headerlink" href="#code-generation-to-code-curation" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;In an AI-first world, junior software engineers become code curators rather than code generators. They will evaluate AI generated solutions for correctness, efficiency and maintainability. They'll identify edge cases the AI misses. They'll take AI generated code and ensure it works within their area of responsibility.&lt;/p&gt;
&lt;h3 id="new-core-competencies"&gt;New Core Competencies&lt;a class="headerlink" href="#new-core-competencies" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;The most successful junior software engineers I've worked with in AI-enabled teams demonstrate the ability to think about entire systems, assess code quality, break down problems clearly, and strive to learn and adapt to new tools which allows them to build a method of debugging code they didn't write themselves.&lt;/p&gt;
&lt;h2 id="looking-forward"&gt;Looking Forward&lt;a class="headerlink" href="#looking-forward" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The software engineering industry is experiencing a shift, but it's not the first time. We've navigated the transition from assembly language to high-level languages, from procedural to object-oriented programming, from desktop to web to mobile development. Each transition created new opportunities for those who adapted thoughtfully.&lt;/p&gt;
&lt;p&gt;The current AI transition is no different, but it requires us to think carefully about what we're optimizing for. If we optimize purely for short-term code generation speed, we risk creating a future where we have powerful AI tools but fewer humans who understand how to use them effectively.&lt;/p&gt;
&lt;h3 id="the-organizations-that-will-win"&gt;The Organizations That Will Win&lt;a class="headerlink" href="#the-organizations-that-will-win" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;The organizations that thrive in this transition will be those that preserve human judgment while leveraging AI capabilities. Organizations that invest in developing people while adopting new tools and capabilities will have a key success factor. These are the organizations that create space for engineers to learn and build engineering thinking into their processes. Most importantly, teams that &lt;a href="https://andrewwegner.com/scaling-teams-without-losing-culture.html"&gt;maintain their cultural&lt;/a&gt; values while adapting processes will find engagement instead of resistance.&lt;/p&gt;
&lt;h3 id="the-individual-path-forward"&gt;The Individual Path Forward&lt;a class="headerlink" href="#the-individual-path-forward" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;For individual engineers, especially those early in their careers, the path forward involves:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Embracing AI as a tool while building problem-solving skills that transcend any specific technology.&lt;/li&gt;
&lt;li&gt;Focusing on system thinking and architectural understanding that AI currently cannot replicate.&lt;/li&gt;
&lt;li&gt;Developing communication skills that allow you to work effectively with both AI tools and human teams.&lt;/li&gt;
&lt;li&gt;Building debugging and quality assessment capabilities that work regardless of who or what generated the code.&lt;/li&gt;
&lt;li&gt;Maintaining curiosity about how things work, not just how to make them work.&lt;/li&gt;
&lt;/ul&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 junior engineer crisis isn't really about AI replacing entry-level engineers. It's about ensuring that as we integrate powerful new tools into our development processes, we don't lose the human elements that create strong engineering teams and effective technical leaders.&lt;/p&gt;
&lt;p&gt;I argue that every significant technology shift creates winners and losers. The winners are those who adapt early and thoughtfully, who understand both the capabilities and limitations of new tools, and who invest in building the human skills that remain uniquely valuable.&lt;/p&gt;
&lt;p&gt;The current moment represents a unique opportunity for engineering leaders to shape how AI integration happens in their organizations. The choices we make now about hiring, training, mentorship, and team structure will determine whether AI makes our engineering teams stronger or simply faster.&lt;/p&gt;
&lt;p&gt;AI is already reshaping our industry. The question is whether we'll guide it in directions that build stronger teams and better engineers, or whether we'll optimize for short-term productivity at the expense of long-term capability.&lt;/p&gt;
&lt;p&gt;The junior software engineers we hire and train today will become the senior engineers leading teams in 2030. How we prepare them for that role, in partnership with AI rather than in replacement by it, may be one of the most important strategic decisions we make as engineering leaders.&lt;/p&gt;
&lt;hr/&gt;
&lt;p&gt;Please join the conversation over on LinkedIn. I've split this article across three posts over there and would love to hear your feedback&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.linkedin.com/posts/andrew-wegner_ai-tools-make-experienced-software-engineers-activity-7363573947457527808-3oRx"&gt;AI tools make experienced software engineers 19% slower&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.linkedin.com/posts/andrew-wegner_ai-is-changing-mentorship-dynamics-in-ways-activity-7363916654793142273-2SOg"&gt;Mentorship dynamics&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.linkedin.com/posts/andrew-wegner_todays-important-question-is-what-skills-activity-7364290146835316737-Chex"&gt;What skills do junior engineers need to be successful alongside AI?&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content><category term="Leadership"/><category term="job"/><category term="leadership"/></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>Leading Distributed Teams: Lessons from Managing Global Engineering Teams</title><link href="https://andrewwegner.com/leading-distributed-teams-lessons.html" rel="alternate"/><published>2025-04-15T01:00:00-05:00</published><updated>2025-04-15T01:00:00-05:00</updated><author><name>Andy Wegner</name></author><id>tag:andrewwegner.com,2025-04-15:/leading-distributed-teams-lessons.html</id><summary type="html">&lt;p&gt;Leading engineers from around the world has taught me how to balance asynchronous communication and decisive action. The right frameworks transform geographical challenges into strategic advantages for organizations seeking to leverage global talent and build resilient engineering cultures.&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;Leading engineering teams across multiple time zones has been a &lt;a href="https://andrewwegner.com/woven-client-to-woven-employee.html"&gt;defining part of my career&lt;/a&gt;. I have taken courses on how to &lt;a href="https://andrewwegner.com/gitlab-manage-remote-team.html"&gt;manage a remote team&lt;/a&gt;, how to implement &lt;a href="https://andrewwegner.com/gitlab-teamops-certification.html"&gt;TeamOps&lt;/a&gt; (both offered by &lt;a href="https://about.gitlab.com/blog/"&gt;GitLab&lt;/a&gt;) and I've managed teams spanning half of the global time zones, with team members located across every continent except Antarctica. This global distribution brings tremendous advantages in terms of talent access and round-the-clock development capability, but it also presents unique leadership challenges that require intentional strategies.&lt;/p&gt;
&lt;p&gt;As software development continues to &lt;a href="https://andrewwegner.com/remote-work-thoughts-about-offices.html"&gt;embrace remote work&lt;/a&gt; - kind of, it has started falling out of favor since the end of the pandemic - the ability to &lt;a href="https://andrewwegner.com/scaling-teams-without-losing-culture.html"&gt;effectively lead and scale distributed engineering teams&lt;/a&gt; has evolved from a nice-to-have skill to a critical competency for senior leaders. The &lt;a href="https://andrewwegner.com/why-remote-work-is-good-for-your-team.html"&gt;lessons I've learned managing global teams&lt;/a&gt; have shaped my leadership approach and provided insights that apply across both to engineering and to the broader organization.&lt;/p&gt;
&lt;h2 id="building-foundations-for-distributed-success"&gt;Building Foundations for Distributed Success&lt;a class="headerlink" href="#building-foundations-for-distributed-success" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The foundation of any successful distributed team begins with &lt;a href="https://andrewwegner.com/top-soft-skills-senior-developers-need.html#excellent-communication"&gt;intentional communication&lt;/a&gt;. Tools like Slack, Zoom (with recording capabilities), Atlassian suite (Jira/Confluence), GitHub/GitLab, and collaborative document platforms like Google Workspace aren't just conveniences. They are the essential connective tissue of distributed teams.&lt;/p&gt;
&lt;p&gt;However, having the right tools is only the beginning. More important is establishing clear principles for how these tools should be used. In my experience, the most successful distributed teams operate with a strong bias toward asynchronous communication. This means documenting decisions thoroughly, creating detailed specifications in advance of implementation, and ensuring appropriate synchronous discussions are recorded and summarized for team members in different time zones. Things like team meetings, troubleshooting sessions, and deep dives should be recorded and shared so that others can consume this information too.&lt;/p&gt;
&lt;p&gt;Documentation is the backbone of effective collaboration. Unlike co-located teams that can rely on impromptu conversations or whiteboard sessions to clarify questions, distributed teams need comprehensive documentation that stands on its own. This includes architectural decisions, specification documents, onboarding guides, and even meeting notes.&lt;/p&gt;
&lt;p&gt;A &lt;a href="https://handbook.gitlab.com/handbook/company/culture/all-remote/remote-work-report/"&gt;study by GitLab in 2021&lt;/a&gt; (during the height of the pandemic) found that less than half of respondents felt their company was sharing company goals, creating and documenting processes and standards or promoting visibility across teams well. Intentional communication is important in all work places, but even more so when everyone is located in geographically different areas of the world.&lt;/p&gt;
&lt;p&gt;I've found that building a "document-first" culture pays enormous dividends. When team members know that the first question in response to "How do I...?" will be "Did you check the documentation?", they naturally begin to contribute to and rely on that shared knowledge base. This reduces repeat questions, accelerates onboarding, and creates a more equitable information environment across time zones.&lt;/p&gt;
&lt;h2 id="decision-making-frameworks-across-time-zones"&gt;Decision-Making Frameworks Across Time Zones&lt;a class="headerlink" href="#decision-making-frameworks-across-time-zones" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;One of the most challenging aspects of leading distributed teams is balancing the need for inclusive decision-making with the necessity to move projects forward. Over time, I've developed a time-gated decision framework that respects input from all regions while preventing decision paralysis.&lt;/p&gt;
&lt;p&gt;For non-emergency decisions, I post proposals to our shared channels with clear timelines for feedback. For example: "I'll be creating Jira stories based on this architectural approach starting next Monday unless concerns are raised." This provides a minimum of one business day (often more) for team members across all regions to review and comment, while also creating a clear decision point that prevents endless deliberation.&lt;/p&gt;
&lt;p&gt;For decisions requiring input from specific individuals, I'll explicitly mention them in the initial message. This creates accountability while also signaling to others who the key stakeholders are for that particular domain.&lt;/p&gt;
&lt;p&gt;Not all decisions can wait, of course. Production outages or customer-impacting bugs demand synchronous communication and rapid response. In these cases, available team members need to be empowered to make decisions with the information at hand, with the understanding that these decisions will be reviewed and possibly refined once more of the team is available.&lt;/p&gt;
&lt;p&gt;The balancing act of knowing when to wait for comprehensive input versus when to move forward with available information is one of the most nuanced skills in distributed leadership. In my experience, being explicit about which type of decision is being made helps team members adjust their expectations appropriately.&lt;/p&gt;
&lt;h2 id="technical-infrastructure-for-global-development"&gt;Technical Infrastructure for Global Development&lt;a class="headerlink" href="#technical-infrastructure-for-global-development" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The technical infrastructure supporting distributed development deserves particular attention. Continuous Integration/Continuous Deployment (CI/CD) pipelines, which are beneficial for any engineering organization, become absolutely essential for distributed teams. Automated testing, code quality checks, and deployment processes create consistency across time zones and reduce the risk of human error. This isn't limited to just a "time zone" thing though. It creates consistency across the team. Everyone's code, from the newest member of the team to the most veteran member, has their code commits go through the exact same process.&lt;/p&gt;
&lt;p&gt;Code review practices need careful consideration in distributed environments. When reviewers might be asleep during a developer's working hours, teams need clear expectations about review timeframes. I emphasize that while we strive for quick reviews, team members should be prepared to either continue with different work or pivot to another task if a review is delayed. As the team grows and more individuals are working similar hours these delays will reduce, but initially, it is a concern that the team members have deliberately work on so that someone isn't always delayed.&lt;/p&gt;
&lt;p&gt;Deployment strategies must also account for global distribution. Whether your team deploys each commit directly to production or follows a more scheduled release cadence, the key is establishing clear, documented processes that anyone on the team can follow regardless of location. When setting up these processes, consider who needs to be available during deployments, how deployment failures will be handled across the team, and whether certain deployment windows should be avoided due to regional holidays or weekend coverage.&lt;/p&gt;
&lt;p&gt;In my experience, the most successful approach is establishing deployment processes that don't require real-time coordination between team members in different time zones. This might mean investing more heavily in automated testing, feature flags, and rollback capabilities, but the resulting independence pays dividends in team efficiency and satisfaction.&lt;/p&gt;
&lt;h2 id="a-support-model"&gt;A Support Model&lt;a class="headerlink" href="#a-support-model" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;One of the natural advantages of a globally distributed team is the ability to provide around-the-clock coverage without requiring anyone to work unusual hours. We've implemented a rotation support model, where team members handle support duties during their local working hours. This creates continuous coverage while respecting everyone's work-life balance. Depending on the product, this could mean that the team only provides support during business hours or it could mean that the support follows the sun. In either case, the type of support provided must be clearly communicated to both the team and to end users.&lt;/p&gt;
&lt;p&gt;This doesn't mean team members are never contacted outside working hours. True emergencies sometimes require waking someone with specialized knowledge. However, two important principles guide these situations:&lt;/p&gt;
&lt;p&gt;First, &lt;a href="https://andrewwegner.com/management-failure-unlimited-pto.html"&gt;if someone is contacted outside working hours to address an issue, they receive compensatory time off&lt;/a&gt;. This creates accountability in the escalation process. People think twice before waking a colleague unless truly necessary.&lt;/p&gt;
&lt;p&gt;Second, and perhaps more importantly, each off-hours escalation triggers a review of our documentation and training. If someone needed to be woken for a particular issue, that indicates a gap in our knowledge distribution that needs to be addressed. This transforms what could be a negative experience into an opportunity for team improvement.&lt;/p&gt;
&lt;p&gt;Over time, this approach creates robust documentation and broader knowledge distribution across the team, reducing the frequency of off-hours disruptions and building resilience into the organization. This is important, because documentation takes time. It's rarely a one shot deal. &lt;/p&gt;
&lt;h2 id="evaluations-in-distributed-teams"&gt;Evaluations in Distributed Teams&lt;a class="headerlink" href="#evaluations-in-distributed-teams" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Evaluating performance fairly across distributed teams requires deliberate attention to measurable outcomes rather than observable behaviors. I focus primarily on deliverables. Are teams hitting their targets and delivering on time and on budget? Are we meeting the KPIs we've established for deployments, code reviews, specifications, feature delivery, and performance improvements?&lt;/p&gt;
&lt;p&gt;For managers specifically, I evaluate whether they're maintaining regular one-on-one meetings with all team members, regardless of location, and whether they're developing clear career succession and training plans for each report. These expectations apply uniformly across the organization, creating consistency in how leadership effectiveness is measured.&lt;/p&gt;
&lt;p&gt;To ensure that geographical distance doesn't create invisible barriers to advancement, I maintain skip-level one-on-ones with team members across all regions. This gives everyone direct access to higher-level leadership and provides me with unfiltered insights into how the team is operating and individual contributions that might otherwise be obscured by distance.&lt;/p&gt;
&lt;p&gt;I've found that managers who excel in distributed environments share certain qualities: they communicate proactively, document thoroughly, avoid making assumptions about team members' contexts, and create equitable opportunities for visibility and recognition regardless of location. These behaviors become key elements in leadership development and promotion criteria.&lt;/p&gt;
&lt;h2 id="developing-talent-globally"&gt;Developing Talent Globally&lt;a class="headerlink" href="#developing-talent-globally" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Hiring for distributed teams requires evaluating candidates not just for technical skills but for their ability to thrive in a remote, asynchronous environment. Beyond the job-specific requirements, I look for clear written and verbal communication skills, as team members will interact with colleagues, leaders, and customers from diverse linguistic and cultural backgrounds. Comfort with asynchronous work patterns and self-direction is essential, as are strong documentation habits that support knowledge sharing. Cultural adaptability and awareness round out the key attributes that predict success in distributed environments.&lt;/p&gt;
&lt;p&gt;Once hired, ensuring equitable career development opportunities across regions becomes essential. During review cycles, we systematically discuss everyone's performance and growth trajectory. I expect managers to speak knowledgeably about each team member's contributions and aspirations, regardless of where they're located.&lt;/p&gt;
&lt;p&gt;The skip-level one-on-ones mentioned earlier serve a dual purpose here. First, they help identify talent that might otherwise be overlooked due to geographical distance from headquarters or senior leadership. These conversations also build relationships that help me understand individual motivations and career goals across the organization.&lt;/p&gt;
&lt;p&gt;One particularly effective practice has been creating cross-regional project teams that give team members exposure to colleagues and challenges outside their immediate locale. This broadens perspectives while creating mentorship and leadership opportunities that might not arise within more regionally confined teams.&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;As I've moved through increasingly senior leadership roles, the principles of effective team management have become even more important to my leadership approach. The future of engineering leadership demands comfort with distributed teams. Demands. Leading a team, whether it's fully remote or hybrid, is a skill that leaders need if they want to have access to talent.&lt;/p&gt;
&lt;p&gt;The most successful technical leaders in this environment will be those who design systems and processes that work asynchronously by default, create cultures where documentation and knowledge sharing are valued and rewarded, and balance inclusive decision-making with the need to move  forward. They will build technical infrastructure that supports distributed development, implement support models that leverage global distribution, ensure performance evaluation focuses on outcomes rather than presence, and develop hiring practices that identify talent suited for distributed work.&lt;/p&gt;
&lt;p&gt;These capabilities represent the future of engineering leadership at the highest levels. For aspiring technical leaders, developing comfort and competence in distributed team management isn't just a nice-to-have—it's increasingly essential preparation for senior technical leadership roles.&lt;/p&gt;</content><category term="Leadership"/><category term="job"/><category term="leadership"/></entry><entry><title>Technical Debt Management: A Strategic Approach</title><link href="https://andrewwegner.com/tech-debt-management-strategic-approach.html" rel="alternate"/><published>2025-04-08T09:00:00-05:00</published><updated>2025-04-08T09:00:00-05:00</updated><author><name>Andy Wegner</name></author><id>tag:andrewwegner.com,2025-04-08:/tech-debt-management-strategic-approach.html</id><summary type="html">&lt;p&gt;After navigating technical leadership roles across startups and established corporations, I've come to view technical debt as both inevitable and manageable. It's the leadership approach to this debt that often determines whether a company can maintain momentum or finds itself grinding to a halt.&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;The term "technical debt" has become a catch-all phrase, it's used to describe anything from poorly written code to outdated systems. But at its core, technical debt represents the very real trade-off of choosing immediate delivery over long-term maintainability. For companies caught between aggressive timelines and the need for sustainable systems, managing this debt becomes more than just a technical challenge. It's a strategic imperative.&lt;/p&gt;
&lt;h2 id="understanding-technical-debt"&gt;Understanding Technical Debt&lt;a class="headerlink" href="#understanding-technical-debt" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Technical debt manifests differently at various companies. Large organizations struggle with legacy systems built decades ago, while smaller companies struggle with systems built hastily last quarter that are already creaking under unexpected scale or scope expansion.&lt;/p&gt;
&lt;p&gt;The primary sources of technical debt during rapid scaling often emerge from several common scenarios. I've witnessed early architectural decisions made with limited information create significant challenges. When we built our first API at one startup, we made assumptions about how users would utilize the API and the portal built on top of it that proved incorrect. We made assumptions about our own product line that didn't hold true as we scaled. As our user base grew, as our product line grew, as our internal structure grew, we found that we'd built ourselves into a cage that wasn't easy to build our way out of. Acquisition integration introduces another layer of complexity, as we often need to integrate systems built with different architectural assumptions and technical standards.&lt;/p&gt;
&lt;p&gt;Feature pressure also plays a huge role. When racing to meet competitive threats or capitalize on market opportunities, corners get cut. These aren't necessarily bad decisions at the time, but they accumulate over time. Team rotation creates knowledge gaps. As teams and companies scale, original architects and developers move into management, other roles, or to other companies taking vital context with them if proper knowledge transfer isn't prioritized.&lt;/p&gt;
&lt;p&gt;The "move fast and break things" ethos that drives early success can quickly become a liability if technical debt isn't managed proactively. In these environments, yesterday's clever hack becomes today's bottleneck, and today's workaround becomes tomorrow's single point of failure.&lt;/p&gt;
&lt;h2 id="the-real-cost-of-technical-debt"&gt;The Real Cost of Technical Debt&lt;a class="headerlink" href="#the-real-cost-of-technical-debt" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Technical debt is often discussed in abstract terms, making it difficult for non-technical stakeholders to appreciate its impact. To make these costs tangible, I find it helpful to categorize them into different areas of impact.&lt;/p&gt;
&lt;p&gt;Unmanaged technical debt progressively slows development. What once took days begins taking weeks as developers navigate around increasingly brittle code. Systems built around technical debt typically require more hands-on management. For example, in one role, we found support was seeing the same pattern of problems over and over again, despite engineering effort to solve the problem. Engineers were spending so much time maintaining these systems that new development slowed to a crawl.&lt;/p&gt;
&lt;p&gt;Which leads to what I think is the most damaging aspect of unmanaged technical debt. Stifled innovation. When engineers spend their creative energy working around limitations rather than solving new problems, competitive advantage erodes. This cost is hardest to quantify but most devastating long-term. Technical debt becomes most visible when it impacts customers. Whether this manifests through outages, performance issues, or security vulnerabilities, it impacts customers in some way. By then, the solution is typically more expensive and disruptive than if addressed proactively.&lt;/p&gt;
&lt;h2 id="when-to-address-vs-when-to-defer"&gt;When to Address vs. When to Defer&lt;a class="headerlink" href="#when-to-address-vs-when-to-defer" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Not all technical debt requires immediate attention. The key is distinguishing between strategic debt that enables necessary velocity and toxic debt that creates compounding problems. Here's how I approach these decisions:&lt;/p&gt;
&lt;h3 id="the-upgrade-case-study"&gt;The Upgrade Case Study&lt;a class="headerlink" href="#the-upgrade-case-study" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;As I mentioned above, at one company, we faced a critical decision about our API infrastructure. We had built an initial API quickly as a skeleton for the product, but it wasn't designed for the scale and feature set we now needed. As the team grew rapidly, we had to decide whether to focus exclusively on building a new production ready API, or gradually migrate from old to new while continuing feature development on both.&lt;/p&gt;
&lt;p&gt;After analysis, we estimated that implementing new product features in the old API would actually take longer than building a robust new API and implementing the new features there first. The tipping point came when our team estimated that one specific new product line would take less time to deliver by building the skeleton of a new API, setting it up properly, and implementing the new feature than it would to build the feature into the current API.&lt;/p&gt;
&lt;p&gt;The clean-cut approach allowed new team members to focus entirely on the new API, infrastructure, and features. They came up to speed quickly by building a product they knew from the ground up. We ported the old API functionality to achieve parity, added new features as an incentive for customers to migrate, and then deprecated and ultimately shut down V1.&lt;/p&gt;
&lt;p&gt;This counterintuitive approach, where addressing technical debt actually accelerated delivery, is exactly the kind of strategic analysis companies need.&lt;/p&gt;
&lt;h3 id="why-early-is-cheaper"&gt;Why Early Is Cheaper&lt;a class="headerlink" href="#why-early-is-cheaper" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;A fundamental principle of technical debt management is that the cost to update almost always increases with time. In my experience, moving one version forward is generally easier than moving two, which is easier than moving three. Waiting until something reaches end-of-life means you're multiple versions behind, increasing the likelihood that functionality you rely on has been deprecated or removed.&lt;/p&gt;
&lt;p&gt;The same principle applies across development. Finding problems in development is cheaper than finding them in QA, which is cheaper than finding them in production. Security follows this curve most dramatically. Addressing vulnerabilities during development is vastly cheaper than responding to a public security breach.&lt;/p&gt;
&lt;p&gt;When discussing technical debt with business stakeholders, this compounding cost curve is the most persuasive argument for proactive management.&lt;/p&gt;
&lt;h2 id="making-technical-debt-visible"&gt;Making Technical Debt Visible&lt;a class="headerlink" href="#making-technical-debt-visible" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Creating organizational awareness around technical debt is essential for managing it effectively. The approaches I've found most effective include visualization tools that make the abstract concrete.&lt;/p&gt;
&lt;p&gt;One simple but powerful approach that worked for us was a "stoplight" system for technical debt. We identified all libraries, frameworks, and language versions in use and tracked their end-of-life dates. These became our "red light" dates - points at which we must update or risk being on unsupported versions. This system helped build a clear priority list and highlighted whether manual maintenance was feasible or automation was needed. Managing 5 dependencies manually is reasonable; managing 500 is not.&lt;/p&gt;
&lt;h2 id="automation-as-visibility"&gt;Automation as Visibility&lt;a class="headerlink" href="#automation-as-visibility" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Tools like &lt;a href="https://docs.github.com/en/code-security/getting-started/dependabot-quickstart-guide"&gt;Dependabot&lt;/a&gt; provide ongoing visibility into dependency status. When automated systems continuously generate pull requests for outdated dependencies, the entire organization develops awareness of technical health. These systems make the invisible nature of tech debt visible and turn theoretical discussions about debt into actionable items. It also does it gradually, so it just becomes part of system maintenance, instead of giant maintenance cycles that slow development.&lt;/p&gt;
&lt;p&gt;When discussing technical debt with non-technical executives, I focus on two key aspects. First is why we need to address it. These reasons vary depending on the scenario and team, but could be something like us being on an unsupported version, or we that spend significant hours weekly working around issues fixed in newer versions.&lt;/p&gt;
&lt;p&gt;The second aspect is considering the consequences of deferring. If we remain on an unsupported version, the vendor won't help when problems arise or at least not cheaply. There might be active exploits in the wild for the version of a library we are using.&lt;/p&gt;
&lt;p&gt;I also include impact analysis on existing priorities. If properly planned, technical debt work fits into existing timelines with minimal disruption. When that's not possible, I clearly explain the trade-offs and impacts of both addressing and deferring the debt.&lt;/p&gt;
&lt;h2 id="cultural-elements-of-technical-debt-management"&gt;Cultural Elements of Technical Debt Management&lt;a class="headerlink" href="#cultural-elements-of-technical-debt-management" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Technical debt management isn't only a technical practice. It's a cultural one too. The most successful approaches I've seen embed debt management into everyday work rather than treating it as an occasional "debt sprint."&lt;/p&gt;
&lt;p&gt;A culture of automating away low-level technical debt proved crucial in my experience. Implementing tools like Dependabot, &lt;a href="https://owasp.org/"&gt;OWASP&lt;/a&gt; scans, and &lt;a href="https://docs.gitlab.com/ci/testing/accessibility_testing/"&gt;accessibility checks&lt;/a&gt; reduced the time engineers spent worrying about these issues because they were handled behind the scenes. This automation-first approach served multiple purposes. It baked prevention into the development process, encouraged engineers to think about security, accessibility, and dependencies early, demonstrated organizational commitment to technical excellence, and created a positive ripple effect as other teams adopted similar practices.&lt;/p&gt;
&lt;p&gt;When one team successfully automated technical debt management, others naturally followed suit. This created a culture of continuous improvement rather than periodic cleanup.&lt;/p&gt;
&lt;p&gt;One particularly effective practice I found was assigning new engineers small technical debt tasks as their first projects. This might be a library update, minor bug fix, or addressing a potential security flaw. New engineers made immediate, valuable contributions while building confidence in a new system by completing manageable tasks. The team addressed technical debt items that might otherwise struggle for prioritization, documentation got updated based on fresh perspectives, and new team members learned team processes in a low-pressure context.&lt;/p&gt;
&lt;p&gt;By introducing technical debt management during onboarding, we established it as a normal part of engineering work instead of an occasional activity.&lt;/p&gt;
&lt;h2 id="the-continuous-approach"&gt;The Continuous Approach&lt;a class="headerlink" href="#the-continuous-approach" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Rather than scheduling occasional "technical debt sprints" that disrupt normal work, I've found greater success in making debt reduction a continuous process.&lt;/p&gt;
&lt;p&gt;Traditional technical debt sprints create several problems. They interrupt feature development momentum, treat technical excellence as exceptional rather than normal, and encourage deferring small fixes that could be handled immediately to "some other time".&lt;/p&gt;
&lt;p&gt;Instead, I've found treating technical debt like regular household maintenance works better. By making small, continuous improvements, you prevent the need for major renovations in most cases.&lt;/p&gt;
&lt;p&gt;With automated tools handling routine updates, engineering teams can focus on substantive technical debt such as infrastructure improvements, major framework upgrades, outdated UI refreshes, and performance bottlenecks. These larger initiatives deserve careful planning and dedicated time, but they should still be approached as part of normal development cycles rather than exceptional activities.&lt;/p&gt;
&lt;p&gt;The most efficient technical debt management strategy I've witnessed is preventing unnecessary debt in the first place. During high-growth periods, implementing automated checks early in the development process caught potential issues before they became embedded in our systems. Clear standards for code quality, security, and architecture provided guidance without imposing unnecessary constraints. The key was establishing these standards as enablers rather than barriers. They helped developers make good decisions quickly rather than slowing them down with bureaucracy. It also eliminates back and forth during code review - "This line is 120 characters", "This function call should have parameters on new lines". Bake that into your linter that every developer must use and those discussions will never need to occur again.&lt;/p&gt;
&lt;p&gt;Some technical investments I've seen pay dividends by preventing debt accumulation. Comprehensive test suites caught regressions early. CI/CD pipelines enforced quality standards. Architecture decision records preserved context. Service boundaries limited the spread of technical compromises. These investments created an environment where quality was the path of least resistance rather than an additional burden.&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;In my experience in multiple leadership roles, when managed effectively, technical debt becomes not just a necessary evil but a strategic tool. By making conscious decisions about when to take on debt and when to pay it down, companies can maintain the velocity needed for market success while building sustainable technical foundations.&lt;/p&gt;
&lt;p&gt;I've found that making debt visible through automation, metrics, and clear communication ensures technical debt is part of strategic conversations. Embedding debt management in culture builds technical excellence into everyday practices rather than occasional initiatives. Focusing on prevention by investing in tools and practices makes it easier to do things right than to create new debt. Being strategic about remediation means addressing debt that limits key business initiatives while deferring debt that doesn't impact current priorities. Measuring the impact by tracking before-and-after metrics demonstrates the value of technical debt reduction.&lt;/p&gt;
&lt;p&gt;As engineering leaders, our role isn't to eliminate all technical debt. That would be neither possible nor desirable. Instead, our job is to manage technical debt as thoughtfully as we would manage financial debt. The goal is to take it on deliberately when the terms make sense, monitoring its impact, and paying it down strategically to maximize long-term value.&lt;/p&gt;</content><category term="Leadership"/><category term="job"/><category term="leadership"/></entry><entry><title>Scaling Engineering Teams: From 5 to 50+ Without Losing Your Culture</title><link href="https://andrewwegner.com/scaling-teams-without-losing-culture.html" rel="alternate"/><published>2025-04-01T10:00:00-05:00</published><updated>2025-04-01T10:00:00-05:00</updated><author><name>Andy Wegner</name></author><id>tag:andrewwegner.com,2025-04-01:/scaling-teams-without-losing-culture.html</id><summary type="html">&lt;p&gt;Scaling engineering teams is about much more than just adding headcount. It's about creating sustainable growth that preserves the core elements of your culture while evolving systems to support a larger organization. Here are a few of my thoughts about the topic.&lt;/p&gt;</summary><content type="html">
&lt;p&gt;After nearly two decades in software development leadership and multiple VP-level roles, I've learned that scaling engineering teams is about much more than just adding headcount. It's about creating sustainable growth that preserves the core elements of your culture while evolving systems to support a larger organization.&lt;/p&gt;
&lt;h2 id="the-challenges-of-rapid-growth"&gt;The Challenges of Rapid Growth&lt;a class="headerlink" href="#the-challenges-of-rapid-growth" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;When scaling engineering teams, I faced two consistent challenges that required thoughtful solutions:&lt;/p&gt;
&lt;h3 id="challenge-1-accelerated-hiring-while-maintaining-quality"&gt;Challenge 1: Accelerated Hiring While Maintaining Quality&lt;a class="headerlink" href="#challenge-1-accelerated-hiring-while-maintaining-quality" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Hiring quickly while finding candidates with the right skillsets is one of the most difficult aspects of scaling. The pressure to fill seats can easily lead to compromising on talent quality or cultural fit. I've found that being crystal clear about technical requirements while remaining flexible about where a candidate's talents can best serve the organization helps thread this needle.&lt;/p&gt;
&lt;p&gt;During &lt;a href="https://andrewwegner.com/ideal-engineering-interviews.html"&gt;interviews&lt;/a&gt;, I focus on diving deep into candidates' past projects rather than relying solely on technical assessments. If an engineer discusses a project and emphasizes backend architecture and database design while only briefly touching on UI elements, this reveals their true areas of expertise. I always follow up by asking whether they want to continue developing in those areas or expand into new ones. Both answers are valuable—they help me understand not just what the candidate can do today, but how they might grow tomorrow.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://andrewwegner.com/top-soft-skills-senior-developers-need.html"&gt;Soft skills are incredibly important&lt;/a&gt;. An engineer that takes ownership of the system, is willing to try an unknown, likes mentoring (and being mentored), and can communicate with others in a professional and empathic manner can bring much more than their technical skills to a growing team. Obviously you need that technical skill, but an engineer isn't a machine. They should be able to do more than sling code, rebuild a server, or make a system diagram. I am hiring someone for their whole set of skills.&lt;/p&gt;
&lt;h3 id="challenge-2-knowledge-transfer-at-scale"&gt;Challenge 2: Knowledge Transfer at Scale&lt;a class="headerlink" href="#challenge-2-knowledge-transfer-at-scale" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;In small teams, knowledge naturally diffuses through daily interactions. As teams grow, this organic process breaks down, creating knowledge silos and redundant work. Each wave of engineers we onboarded faced steeper learning curves without proper documentation and orientation.
Our solution was to make each cohort of new hires part of the knowledge-building process. Every wave of engineers contributed to our knowledge bases, improving the experience for the next group. We implemented, essentially, an onboarding buddy system that paired new engineers with each other to build relationships, while also connecting them with experienced team members who could fill in knowledge gaps.&lt;/p&gt;
&lt;p&gt;The other benefit of these types of improvements, is that it supports &lt;a href="https://andrewwegner.com/remote-work-thoughts-about-offices.html"&gt;asynchronous communication and remote work&lt;/a&gt;. Remote work and hybrid work have grown in popularity, especially since 2020. Building knowledge transfers through tooling that doesn't expect the entire team to be in a single location provides so much &lt;em&gt;context&lt;/em&gt; to others. Someone can catch up in a Slack channel later, or read a Confluence article tomorrow. &lt;/p&gt;
&lt;h2 id="culture-preservation-techniques-that-work"&gt;Culture Preservation Techniques That Work&lt;a class="headerlink" href="#culture-preservation-techniques-that-work" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Maintaining culture during growth requires intentionality. Here are strategies I've found effective:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Immediate Integration into Meaningful Work: New team members were immediately included in team ceremonies and assigned meaningful tasks—not just busywork. This sent a clear message: you're a valued contributor from day one. By giving new engineers real problems to solve immediately, they quickly identified with the team's mission and felt ownership of their work.&lt;/li&gt;
&lt;li&gt;Public Knowledge Sharing: We instituted a rule that all support work must occur in public channels. This transparency accomplished several things. New engineers could observe troubleshooting in real-time allowing everyone to learn from each issue that arose. Team members could spot when someone was solving a previously addressed problem which helped find patterns of recurring issues. These became visible, highlighting systemic problems that needed permanent fixes.&lt;/li&gt;
&lt;li&gt;Visual Documentation: A picture truly is worth a thousand words on day one. System architecture diagrams, workflow charts, and even team organization maps help new engineers orient themselves quickly. We continually updated these visual aids as our systems and teams evolved.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="evolution-of-team-structure-and-communication"&gt;Evolution of Team Structure and Communication&lt;a class="headerlink" href="#evolution-of-team-structure-and-communication" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;h3 id="adaptive-team-structures"&gt;Adaptive Team Structures&lt;a class="headerlink" href="#adaptive-team-structures" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;There's no one-size-fits-all approach to team structure during scaling phases. I've built dedicated backend, frontend, and QA teams, and I've also created cross-functional pods. The right approach depends on your product, business needs, and the existing strengths of your team.
What matters most is acknowledging that your structure must evolve as you grow. The flat organization that worked at 5 engineers simply won't function at 50. Being transparent about these necessary changes helps the team understand why restructuring happens.&lt;/p&gt;
&lt;h3 id="communication-cascade"&gt;Communication Cascade&lt;a class="headerlink" href="#communication-cascade" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;As teams grow, communication needs to flow through multiple channels. While I always strive to stay hands-on and close to the work, the reality of executive leadership means I can't directly communicate with everyone daily. Building a strong communication cascade through direct managers becomes essential.&lt;/p&gt;
&lt;p&gt;I maintain regular one-on-one meetings with my direct reports and hold skip-level meetings on a consistent cadence. I also encourage my direct reports to do the same with their teams. This creates multiple pathways for information to flow up and down the organization.
Crucially, engineers need to see their feedback traveling up the chain and resulting in actual changes. When team members witness their input driving decisions, they remain engaged in the communication process and feel valued for their insights.&lt;/p&gt;
&lt;h2 id="measuring-success-beyond-headcount"&gt;Measuring Success Beyond Headcount&lt;a class="headerlink" href="#measuring-success-beyond-headcount" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Adding engineers is easy—ensuring they contribute effectively is the real challenge. Here are metrics I've found valuable for measuring successful scaling:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Time to first meaningful commit: How quickly can a new engineer contribute value?&lt;/li&gt;
&lt;li&gt;Feature deployment velocity: Is delivery speed maintaining or improving as the team grows?&lt;/li&gt;
&lt;li&gt;New product launch timelines: Are we bringing innovations to market faster?&lt;/li&gt;
&lt;li&gt;Support queue metrics: Are new engineers getting involved in support while maintaining quality and response times?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These indicators help identify whether your growing team is becoming more capable or just more numerous.&lt;/p&gt;
&lt;h2 id="the-lesson-id-apply-to-future-scaling-efforts"&gt;The Lesson I'd Apply to Future Scaling Efforts&lt;a class="headerlink" href="#the-lesson-id-apply-to-future-scaling-efforts" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;If I were scaling a team again tomorrow, I'd place even greater emphasis on breaking down business expectations into clearly defined incremental goals. Saying "we're adding X engineers to deliver Y major initiative" creates unrealistic timelines and pressure.
New engineers need time to contribute meaningfully to large tasks. By defining a granular priority list with visible intermediate steps, both new and existing engineers can march toward tangible milestones rather than an intimidating "big thing" with unclear pathways to completion.&lt;/p&gt;
&lt;h2 id="cross-team-integration-from-day-one"&gt;Cross-Team Integration from Day One&lt;a class="headerlink" href="#cross-team-integration-from-day-one" title="Permanent link"&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Finally, I've learned the critical importance of ensuring engineering teams aren't isolated from other departments. As engineers onboard and learn, they should continuously interact with product, sales, customer success, and other functions.
This cross-functional exposure provides vital context about why certain features are requested or designed in specific ways. Engineers who understand the business rationale make better technical decisions and can anticipate future needs.
For example, understanding upcoming business requirements helps engineers build flexibility into systems where it will actually be needed, rather than adding unnecessary complexity. Building with the right level of flexibility now is infinitely easier than redesigning systems later.&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;Scaling engineering teams successfully requires balancing technical excellence, cultural preservation, and business alignment. By implementing thoughtful onboarding processes, maintaining transparent communication, measuring meaningful metrics, and keeping engineers connected to the broader business context, you can grow your team without losing what made it special in the first place.
The journey from 5 to 50+ engineers is challenging, but with intentional leadership, it can result in not just a larger team, but a more capable, cohesive and impactful organization.&lt;/p&gt;</content><category term="Leadership"/><category term="job"/><category term="leadership"/></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></feed>