<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: DevLog</title>
    <description>The latest articles on DEV Community by DevLog (@devlog).</description>
    <link>https://dev.to/devlog</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4086093%2F5d250487-05f7-49bc-ac26-846bc42538b1.jpg</url>
      <title>DEV Community: DevLog</title>
      <link>https://dev.to/devlog</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/devlog"/>
    <language>en</language>
    <item>
      <title>Search Console: When sitemap index shows 0</title>
      <dc:creator>DevLog</dc:creator>
      <pubDate>Thu, 01 Oct 2026 11:43:33 +0000</pubDate>
      <link>https://dev.to/devlog/search-console-when-sitemap-index-shows-0-40kp</link>
      <guid>https://dev.to/devlog/search-console-when-sitemap-index-shows-0-40kp</guid>
      <description>&lt;p&gt;I spend a lot of time digging into technical glitches, often running automation scripts on my Mac mini to isolate points of failure. Recently, I encountered a confusing issue in Google Search Console where my sitemap status seemed inaccurate, leading me down a rabbit hole of checking HTML sources and API responses.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why does the sitemap screen show 0 indexed pages?
&lt;/h2&gt;

&lt;p&gt;For several weeks, my sitemaps screen displayed "Submitted: 52" and "Indexed: 0". I had submitted both &lt;code&gt;sitemap.xml&lt;/code&gt; and the Tistory-generated &lt;code&gt;rss&lt;/code&gt; feed, and both were marked as "Success". It felt like something was broken; I started questioning if my &lt;code&gt;google-site-verification&lt;/code&gt; code was missing or if the URLs were incorrect.&lt;/p&gt;

&lt;p&gt;To verify, I used the Search Console API to run &lt;code&gt;sitemaps().list()&lt;/code&gt;. The results showed 55 items for &lt;code&gt;sitemap.xml&lt;/code&gt; and 11 for &lt;code&gt;rss&lt;/code&gt;, both returning HTTP 200 status codes. In my automation scripts, a &lt;code&gt;200 OK&lt;/code&gt; signal indicates the process should move to the next step, so technically, everything was working perfectly. Despite this, the sitemap dashboard stubbornly refused to move past 0 indexed pages.&lt;/p&gt;

&lt;p&gt;In reality, the "Indexed" count on the sitemap screen is an indirect indicator. It reflects whether the sitemap submission was successful or how Google discovered the files, but it is not a real-time mirror of whether your pages are live in search results. Especially on platforms like Tistory, there is often a lag between the sitemap report and actual indexing. For example, as of September 4, 2026, my Tistory page index report was 12 days behind, even though the content was already live.&lt;/p&gt;

&lt;h2&gt;
  
  
  How can I verify if a specific URL is actually indexed?
&lt;/h2&gt;

&lt;p&gt;Since the sitemap dashboard was unreliable, I turned to the "URL Inspection" tool in the Search Console top search bar. This is the only way to get a definitive answer for a specific page.&lt;/p&gt;

&lt;p&gt;To check your status, copy the full URL of your post—for example, &lt;code&gt;https://yourblog.com/8&lt;/code&gt;—and paste it into the URL Inspection box at the top of the console, then hit enter. When I did this for my posts at &lt;code&gt;/8&lt;/code&gt;, &lt;code&gt;/18&lt;/code&gt;, and &lt;code&gt;/44&lt;/code&gt;, the result was clearly "URL is on Google" or "Submitted and indexed."&lt;/p&gt;

&lt;p&gt;This confirmed that while the sitemap screen showed 0, my pages were actually indexed and searchable. In one specific check, I found that 13 posts published between September 11 and September 14 were all "Indexed," with the crawl date matching the publication date, even though the sitemap report had not yet updated.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do I make new posts get indexed faster?
&lt;/h2&gt;

&lt;p&gt;Once I realized the dashboard numbers were misleading, I focused on discovery speed. Google can process multiple sitemaps, and I found that submitting two different types of feeds helps new content get picked up more quickly.&lt;/p&gt;

&lt;p&gt;The first is &lt;code&gt;sitemap.xml&lt;/code&gt;, the standard full list of posts provided by the blog service. The second is the &lt;code&gt;rss&lt;/code&gt; feed (or &lt;code&gt;atom.xml&lt;/code&gt;), which serves as a feed for the most recent posts. To set this up, go to the "Sitemaps" menu in Search Console and submit &lt;code&gt;sitemap.xml&lt;/code&gt;, then repeat the process for &lt;code&gt;rss&lt;/code&gt;. Currently, my &lt;code&gt;rss&lt;/code&gt; shows 11 successful items and &lt;code&gt;sitemap.xml&lt;/code&gt; shows 55.&lt;/p&gt;

&lt;p&gt;If you want to manually push a page, you can use the "Request Indexing" button in the UI. However, this is limited to about 10 requests per day. Google explicitly warns that "submitting multiple times does not change priority," so there is no point in spamming the button. Submitting both sitemap types and verifying via URL Inspection is a much more efficient workflow.&lt;/p&gt;

&lt;p&gt;Stop stressing over the numbers on the sitemap screen and simply check your URLs individually.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://aisideincomelab.blogspot.com/2026/09/search-console-when-sitemap-index-shows.html" rel="noopener noreferrer"&gt;Homelab Notes&lt;/a&gt; — notes from one Mac mini running local LLMs and 24/7 automation.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>contentops</category>
      <category>automation</category>
      <category>macos</category>
    </item>
    <item>
      <title>Claude Code: Analyzing 159 Feedback Memory Files</title>
      <dc:creator>DevLog</dc:creator>
      <pubDate>Wed, 30 Sep 2026 06:43:29 +0000</pubDate>
      <link>https://dev.to/devlog/claude-code-analyzing-159-feedback-memory-files-27ln</link>
      <guid>https://dev.to/devlog/claude-code-analyzing-159-feedback-memory-files-27ln</guid>
      <description>&lt;p&gt;Symptom / Cause / Fix&lt;br&gt;
Repeated mistakes despite automatic memory / Claude reads only the one-line index summary at session start, not the full body / Include the "Why" directly in the one-line summary.&lt;/p&gt;

&lt;p&gt;I've been using Claude Code across my work laptop and home Mac mini for about 15 months. Claude Code includes an automatic memory feature: whenever I correct the AI, it creates a &lt;code&gt;feedback&lt;/code&gt; file. Each file contains a one-line explanation of "why" the correction happened and "how" to apply it next time. While migrating these files from my work laptop, I realized I had accumulated 159 of them—meaning I've had to correct Claude 159 times.&lt;/p&gt;

&lt;p&gt;Reviewing the files labeled as "reoccurred twice," "strongly repeated correction," or "repeated thinking error," I found that the issues fall into five distinct patterns. Interestingly, none of these concern specific code syntax; they are all about the workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Solving in fragments without context
&lt;/h2&gt;

&lt;p&gt;The most frequent correction was telling Claude to stop solving tasks piece-by-piece and instead grasp the overall context. This manifests in three ways:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Claiming to understand requirements but omitting them during implementation.&lt;/li&gt;
&lt;li&gt;Deciding on an answer prematurely and inventing new justifications to defend that conclusion when challenged, rather than re-evaluating the conclusion itself.&lt;/li&gt;
&lt;li&gt;Taking the shortest path to complete an immediate instruction, which effectively ignores the broader context.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  2. Confusing reporting with actual reflection
&lt;/h2&gt;

&lt;p&gt;There is often a gap between Claude claiming something is done and it actually being finished. In one instance, Claude reported "it's on the user's PC" after committing code, forgetting that pushing was still required. This led to two specific memory files: "Work is only complete after merging to main and pushing" and "Verify the remote state again after pushing."&lt;/p&gt;

&lt;p&gt;The same applies to testing and UI. I had to mark a "reoccurred twice" error regarding tests: "A new test is only considered passed if it actually fails after reverting the fix." A test that always passes isn't a test. For UI, the rule is: "It is incomplete until a real screenshot is viewed; an agent's 'design complete' report is merely functional verification."&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Trusting its own eyes over the user's
&lt;/h2&gt;

&lt;p&gt;Claude often confuses its terminal output with what I see in the GUI. It once mistook a Korean character encoding glitch in its own PowerShell capture for a product bug and tried to fix the console encoding. On my Mac mini, it insisted a string didn't exist because its internal count was wrong, failing four times in one day. The rules are now: "Do not doubt what the user sees in their environment" and "Doubt your own inspection before claiming something isn't there."&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Overstepping authority boundaries
&lt;/h2&gt;

&lt;p&gt;"Diligence" is a virtue unless it's directed at a production server. I had to set strict boundaries:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"Review" means review only; implementation starts only after an explicit instruction to proceed.&lt;/li&gt;
&lt;li&gt;Use only &lt;code&gt;GET&lt;/code&gt; on production servers. One "confirmation" &lt;code&gt;POST&lt;/code&gt; once accidentally triggered two crawlers simultaneously.&lt;/li&gt;
&lt;li&gt;Any operation with permanent effects on external systems requires a two-step safety mechanism and visual verification.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  5. The Korean Windows encoding trap
&lt;/h2&gt;

&lt;p&gt;Working on a company PC introduced specific encoding hurdles that reoccurred multiple times. The rules seem contradictory, which is why they must be documented:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;.bat&lt;/code&gt; files for Korean Windows must be saved as &lt;code&gt;CP949&lt;/code&gt;, otherwise execution fails.&lt;/li&gt;
&lt;li&gt;Printing characters like the em dash to the production PC console causes the tool to crash due to encoding errors.&lt;/li&gt;
&lt;li&gt;Cron script output must be fixed to &lt;code&gt;UTF-8&lt;/code&gt;. Reverting this to &lt;code&gt;CP949&lt;/code&gt; wipes out all Korean notifications (this reoccurred twice in August).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To fix these recurring issues, I changed how I manage memory files:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Identify the repeated failure pattern in the &lt;code&gt;feedback&lt;/code&gt; file.&lt;/li&gt;
&lt;li&gt;Rewrite the index summary to include not just the "what," but the "why."&lt;/li&gt;
&lt;li&gt;Verify that the one-line summary is punchy enough to trigger the correct behavior in a new session.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Having 159 memory files isn't a badge of honor; it means the system is leaky. The reason "reoccurred twice" labels exist is that writing something down isn't the same as it being read. Claude Code only reads the one-line summary of the index file when starting a session, not the full body. Now, when saving memory, I prioritize the summary line over the detailed content to ensure it actually works in the next session.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://aisideincomelab.blogspot.com/2026/09/claude-code-analyzing-159-feedback.html" rel="noopener noreferrer"&gt;Homelab Notes&lt;/a&gt; — notes from one Mac mini running local LLMs and 24/7 automation.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>troubleshooting</category>
      <category>clitools</category>
      <category>macos</category>
    </item>
    <item>
      <title>Claude Code PDCA: Why 100% alignment is not success</title>
      <dc:creator>DevLog</dc:creator>
      <pubDate>Tue, 29 Sep 2026 10:43:26 +0000</pubDate>
      <link>https://dev.to/devlog/claude-code-pdca-why-100-alignment-is-not-success-152k</link>
      <guid>https://dev.to/devlog/claude-code-pdca-why-100-alignment-is-not-success-152k</guid>
      <description>&lt;p&gt;Running six PDCA (Plan-Design-Do-Check-Act) cycles with Claude Code on a color extraction tool taught me that high alignment rates are often vanity metrics. In my case, half the cycles were essentially wasted because I relied too heavily on whether the code matched the design.&lt;/p&gt;

&lt;p&gt;The biggest realization was that alignment and effectiveness are two different axes. In one cycle, gap analysis showed 100% alignment between the design document and the implementation; however, zero cases were actually fixed. The AI followed the design perfectly, but the design failed to solve the problem. Now, I track two separate metrics: Axis A (how well the design was met) and Axis B (whether the hypothesis in the plan actually worked).&lt;/p&gt;

&lt;p&gt;I also learned that post-processing cannot fix failures in the upstream process. I spent two cycles trying to fix missing colors by adjusting filters, which failed because the clustering stage wasn't generating those colors in the first place. Now, any plan to modify a filter must first verify that the upstream process is producing the target. Similarly, introducing weights to vivid pixels—intended to improve clustering—actually pulled the center toward outliers, increasing the error in the hardest cases from 20 to 45.&lt;/p&gt;

&lt;p&gt;Synthetic data proved to be another trap. While actual images missed colors in eight out of fourteen cases, a synthetic verification tool caught only one out of eight. The synthetic data was too clean, lacking the gradients and compression noise found in reality. I now require a check to ensure synthetic data statistics are within 10% of real-world data before adopting it as an MVP.&lt;/p&gt;

&lt;p&gt;Interestingly, not every task requires a design document. For simple UI features with clear requirements in the plan, jumping straight to implementation still yielded 98% alignment. This lesson mirrored a Mac Mini review project where five rounds of AI script audits failed for the same reason: generalizing a single constant without conditions.&lt;/p&gt;

&lt;p&gt;Ultimately, PDCA isn't about making the AI write documents; it is a mechanism to force objective judgment. Alignment tells you if the AI followed its own rules, but only reality tells you if it worked.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://aisideincomelab.blogspot.com/2026/09/claude-code-pdca-why-100-alignment-is.html" rel="noopener noreferrer"&gt;Homelab Notes&lt;/a&gt; — notes from one Mac mini running local LLMs and 24/7 automation.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>troubleshooting</category>
      <category>clitools</category>
      <category>macos</category>
    </item>
    <item>
      <title>Claude CLI 401 Unauthorized Refresh Token Issue</title>
      <dc:creator>DevLog</dc:creator>
      <pubDate>Mon, 28 Sep 2026 11:43:23 +0000</pubDate>
      <link>https://dev.to/devlog/claude-cli-401-unauthorized-refresh-token-issue-4g0e</link>
      <guid>https://dev.to/devlog/claude-cli-401-unauthorized-refresh-token-issue-4g0e</guid>
      <description>&lt;p&gt;Symptom: &lt;code&gt;claude&lt;/code&gt; commands fail with &lt;code&gt;401 Unauthorized&lt;/code&gt; and the message "refresh token was already used. Please log out and sign in again."&lt;/p&gt;

&lt;p&gt;Cause: The refresh token expired after being consumed by another session or a background process. Refresh tokens are single-use credentials.&lt;/p&gt;

&lt;p&gt;Fix: Run &lt;code&gt;claude auth login&lt;/code&gt; to refresh authentication without running a logout command first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why claude login alone did not clear the 401
&lt;/h2&gt;

&lt;p&gt;I was running a pipeline command when every subsequent CLI call failed. The terminal returned this exact output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Error: 401 Unauthorized
  Message: "Authentication failed: refresh token was already used. Please log out and sign in again."
  Code: "AUTH_ERROR"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The error text explicitly instructs me to log out. I spent roughly ten minutes searching the terminal for a logout function. The prompt only showed &lt;code&gt;AUTH_ERROR&lt;/code&gt;. I assumed clearing old sessions was required first. That assumption blocked progress. The instructions in the error message describe your token state, not the recovery step. You do not need to run &lt;code&gt;claude auth logout&lt;/code&gt;. The token is already invalid. Executing a logout command on an expired session adds unnecessary steps without changing the result. You only need to sign in again.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to verify a consumed refresh token state
&lt;/h2&gt;

&lt;p&gt;I checked available commands by running &lt;code&gt;claude auth --help&lt;/code&gt;. It listed three options: &lt;code&gt;login&lt;/code&gt;, &lt;code&gt;logout&lt;/code&gt;, and &lt;code&gt;status&lt;/code&gt;. I used &lt;code&gt;claude auth status&lt;/code&gt; to verify the broken state. It confirmed the session was inactive. This command is useful for verifying your fix works immediately after re-authenticating.&lt;/p&gt;

&lt;p&gt;The core issue lies in how refresh tokens work. A refresh token is a single-use credential that keeps your session alive without asking for fresh credentials repeatedly. If any background script or another terminal window consumes it, the token becomes invalid. Your active session loses its validity instantly. The error message mentions &lt;code&gt;log out&lt;/code&gt; because the system expects you to clean up after yourself, but that cleanup function only works on active sessions. When the token is already consumed, you must bypass the logout step entirely.&lt;/p&gt;

&lt;h2&gt;
  
  
  What happens when background scripts burn tokens
&lt;/h2&gt;

&lt;p&gt;The CLI functioned normally until a pipeline execution triggered the failure. No manual logout occurred beforehand. This pointed to an unseen consumer. Long-running scripts or suspended processes often hold onto refresh tokens in the background. When they execute, they use the token to verify their identity against the API. Once that happens, your active terminal session can no longer use it. The token is burned.&lt;/p&gt;

&lt;p&gt;I resolved the issue by running &lt;code&gt;claude auth login&lt;/code&gt;. The terminal opened a browser window to complete the authentication flow. Alternatively, it prompted me directly in the terminal for an authentication code. I completed the prompt. All commands immediately resumed working. The &lt;code&gt;401&lt;/code&gt; error disappeared. I verified everything was normal by running &lt;code&gt;claude auth status&lt;/code&gt;. It showed an active session. The entire process took less than two minutes once I stopped looking for a logout function.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to prevent token exhaustion during pipeline runs
&lt;/h2&gt;

&lt;p&gt;I learned a few concrete things from this breakdown. Session management requires understanding how tokens behave under the hood. I will keep this checklist for future pipeline maintenance:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Ignore &lt;code&gt;log out&lt;/code&gt; instructions when you see a consumed token error.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Run &lt;code&gt;claude auth login&lt;/code&gt; immediately to generate a fresh token.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Use &lt;code&gt;claude auth status&lt;/code&gt; to verify your session is active before running pipelines.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Check for long-running background scripts that might be burning refresh tokens.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;em&gt;Written from real hands-on experience, drafted with AI assistance.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Related posts
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;npm command not found on Windows: fix the PATH&lt;/li&gt;
&lt;li&gt;codex CLI login not working on macOS: what fixed it&lt;/li&gt;
&lt;li&gt;Run a Discord bot 24/7 for free on a computer you already own&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://aisideincomelab.blogspot.com/2026/09/claude-cli-401-unauthorized-refresh.html" rel="noopener noreferrer"&gt;Homelab Notes&lt;/a&gt; — notes from one Mac mini running local LLMs and 24/7 automation.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>clitools</category>
      <category>localllm</category>
      <category>troubleshooting</category>
    </item>
    <item>
      <title>codex CLI login not working on macOS: what fixed it</title>
      <dc:creator>DevLog</dc:creator>
      <pubDate>Mon, 28 Sep 2026 07:43:22 +0000</pubDate>
      <link>https://dev.to/devlog/codex-cli-login-not-working-on-macos-what-fixed-it-2fi2</link>
      <guid>https://dev.to/devlog/codex-cli-login-not-working-on-macos-what-fixed-it-2fi2</guid>
      <description>&lt;p&gt;TL;DR:&lt;/p&gt;

&lt;p&gt;Symptom: &lt;code&gt;codex --version&lt;/code&gt; fails with "No such file or directory", while &lt;code&gt;codex login&lt;/code&gt; and &lt;code&gt;codex exec&lt;/code&gt; throw 401 Unauthorized or "Not inside a trusted directory".&lt;/p&gt;

&lt;p&gt;Cause: brew cask leaves only symlinks with missing binaries, expired tokens in auth.json, and security restrictions blocking non-git directories.&lt;/p&gt;

&lt;p&gt;Fix: Run &lt;code&gt;brew reinstall --cask codex&lt;/code&gt; to restore the binary, re-run &lt;code&gt;codex login&lt;/code&gt; for a fresh token, and append --skip-git-repo-check when working outside repositories.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why codex login alone did not clear the 401
&lt;/h2&gt;

&lt;p&gt;I spent hours trying to get the codex CLI working after a clean install. The first red flag appeared when I ran &lt;code&gt;codex --version&lt;/code&gt;. Instead of printing the installed version, it immediately crashed with "No such file or directory". This is a classic symptom of a broken package manager installation. When you install tools through brew cask, the system sometimes only writes symbolic links instead of copying the actual executable files. A symbolic link is just a reference path that points to another file, so if the target disappears, the link becomes useless. It felt like checking a skeleton and finding nothing inside.&lt;/p&gt;

&lt;p&gt;Running &lt;code&gt;brew reinstall --cask codex&lt;/code&gt; fixed the missing binary and restored the symlinks. &lt;code&gt;codex --version&lt;/code&gt; started working again, which made me confident I had fixed the installation. Then came &lt;code&gt;codex login&lt;/code&gt;. Every attempt returned 401 Unauthorized. I assumed the API key was invalid and went through the reissuing process, but the error persisted. The real issue lived inside auth.json, which stores your authentication tokens on disk. Authentication tokens are temporary credentials that prove your identity to the service. Package updates or manual edits can leave these tokens expired on disk. Running &lt;code&gt;codex login&lt;/code&gt; again forces the CLI to generate a fresh token and overwrite the stale entry. It cleared the 401 Unauthorized response immediately.&lt;/p&gt;

&lt;h2&gt;
  
  
  Breaking the trusted directory check for local projects
&lt;/h2&gt;

&lt;p&gt;The final hurdle appeared when I tried &lt;code&gt;codex exec&lt;/code&gt;. Running it outside a version-controlled project triggered "Not inside a trusted directory" and refused to execute. This is not a bug; it is a built-in security measure. The CLI defaults to running only in directories that contain git commit history, assuming those are validated codebases. When I tried to apply the tool to private files or scratch projects, it blocked me without warning.&lt;/p&gt;

&lt;p&gt;I had to dig into &lt;code&gt;man codex&lt;/code&gt; to find the override. Adding --skip-git-repo-check disables that directory validation and lets the command run anywhere you specify. Testing &lt;code&gt;codex exec --skip-git-repo-check&lt;/code&gt; confirmed it worked exactly as intended. After verifying that &lt;code&gt;codex login&lt;/code&gt; succeeded and &lt;code&gt;codex --version&lt;/code&gt; printed correctly, I stopped fighting the toolchain.&lt;/p&gt;

&lt;p&gt;Dealing with broken symlinks, expired tokens, and hidden security checks simultaneously taught me a practical lesson. CLI failures rarely come from a single missing dependency. Package managers, local config files, and default security policies interact in ways that simple reinstallation cannot fix. You have to trace each error back to its actual source before applying the next command. Understanding how your package manager and tool defaults interact saves more time than memorizing error codes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Related posts
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Run a Discord bot 24/7 for free on a computer you already own&lt;/li&gt;
&lt;li&gt;npm command not found on Windows: fix the PATH&lt;/li&gt;
&lt;li&gt;Claude CLI 401 Unauthorized Refresh Token Issue&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://aisideincomelab.blogspot.com/2026/09/fix-codex-cli-login-and-brew-cask.html" rel="noopener noreferrer"&gt;Homelab Notes&lt;/a&gt; — notes from one Mac mini running local LLMs and 24/7 automation.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>clitools</category>
      <category>macos</category>
      <category>troubleshooting</category>
    </item>
    <item>
      <title>Run a Discord bot 24/7 for free on a computer you already own</title>
      <dc:creator>DevLog</dc:creator>
      <pubDate>Sun, 27 Sep 2026 10:43:18 +0000</pubDate>
      <link>https://dev.to/devlog/run-a-discord-bot-247-for-free-on-a-computer-you-already-own-2on6</link>
      <guid>https://dev.to/devlog/run-a-discord-bot-247-for-free-on-a-computer-you-already-own-2on6</guid>
      <description>&lt;p&gt;Once you have a Discord bot working, the next problem shows up fast. Close the terminal and the bot dies. Turn off the computer and it dies. So you go looking for "free 24/7 hosting".&lt;/p&gt;

&lt;p&gt;Before you do: &lt;strong&gt;if you have a computer at home that stays on, that is your host.&lt;/strong&gt; Ours is a Mac mini that has been up 34 days straight. Hosting cost is zero.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you get by hosting it yourself
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;It never sleeps. Free tiers often idle your bot out. Waking up takes time, and commands sent in between are lost&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;It can touch your files. The bot runs inside your machine, so your folders, your scripts and your installed tools are all available. This is the real difference&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;No expiry. Free plans change. Your own computer does not&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;You pay for electricity, nothing else&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The trade is that you are tied to your home power and internet. A blackout takes the bot with it. For a bot &lt;strong&gt;you&lt;/strong&gt; use, that is usually fine.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: survive closing the terminal
&lt;/h2&gt;

&lt;p&gt;A program started in a terminal belongs to that window. Close it and the program goes too.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;screen&lt;/code&gt; gives you a detached place to run it. macOS ships with it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;screen &lt;span class="nt"&gt;-dmS&lt;/span&gt; mybot python3 /Users/me/bot.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;-dmS&lt;/code&gt; means "start detached, give it a name". You can close the terminal now.&lt;/p&gt;

&lt;p&gt;To look in on it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;screen &lt;span class="nt"&gt;-ls&lt;/span&gt;
screen &lt;span class="nt"&gt;-r&lt;/span&gt; mybot
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;screen -ls&lt;/code&gt; prints something like this.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;There is a screen on:
        37448.mybot     (Detached)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Detached&lt;/code&gt; means nobody is watching but it is running. &lt;code&gt;screen -r&lt;/code&gt; attaches you to it. &lt;strong&gt;To leave, press Ctrl+A then D. Pressing Ctrl+C kills the bot.&lt;/strong&gt; That catches everyone once.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;tmux&lt;/code&gt; does the same job. &lt;code&gt;nohup&lt;/code&gt; is simpler but you cannot look at the screen later, which you will want.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: survive a reboot
&lt;/h2&gt;

&lt;p&gt;A screen session dies with the machine, and updates reboot it whether you like it or not.&lt;/p&gt;

&lt;p&gt;On macOS, &lt;code&gt;launchd&lt;/code&gt; handles this. You write one file, register it, and the thing starts at login and restarts if it dies. On Linux it is &lt;code&gt;systemd&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: the shape that actually works
&lt;/h2&gt;

&lt;p&gt;Here is where people usually go wrong. You &lt;strong&gt;can&lt;/strong&gt; have &lt;code&gt;launchd&lt;/code&gt; start the bot directly. We do not.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;launchd&lt;/code&gt; starts a small script, and that script manages the screen session.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The reason is that you will want to see the bot's screen with your own eyes. A process started directly by &lt;code&gt;launchd&lt;/code&gt; has no screen to attach to. Put &lt;code&gt;screen&lt;/code&gt; in the middle and &lt;code&gt;screen -r&lt;/code&gt; works whenever you need it.&lt;/p&gt;

&lt;p&gt;The script is not complicated.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; screen &lt;span class="nt"&gt;-ls&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s2"&gt;mybot[[:space:]]"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;screen &lt;span class="nt"&gt;-dmS&lt;/span&gt; mybot python3 /Users/me/bot.py
  &lt;span class="k"&gt;fi
  &lt;/span&gt;&lt;span class="nb"&gt;sleep &lt;/span&gt;30
&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every 30 seconds it checks whether the session exists and starts it if not. &lt;code&gt;launchd&lt;/code&gt; keeps the script alive, so if the script dies it comes back. Two layers.&lt;/p&gt;

&lt;p&gt;Add a network check before the first start. Right after a reboot the network is often not up yet, and the bot fails immediately if it tries to connect.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: stop the machine from sleeping
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;If you skip this, your bot dies every night and you will not know why.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Turn off sleep in Energy Saver. You can confirm from the terminal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pmset &lt;span class="nt"&gt;-g&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;sleep 0&lt;/code&gt; means it will not sleep. &lt;code&gt;displaysleep&lt;/code&gt; can stay on — the screen turning off does not stop anything. On a laptop, check what closing the lid does. This is why a headless box like a Mac mini suits the job.&lt;/p&gt;

&lt;h2&gt;
  
  
  The failure nobody plans for
&lt;/h2&gt;

&lt;p&gt;Everything above handles a bot that &lt;strong&gt;dies&lt;/strong&gt;. In practice the worse case is different.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The process is alive and the bot simply stops answering.&lt;/strong&gt; Discord still shows it online. &lt;code&gt;ps&lt;/code&gt; still lists it. Commands get no reply. A session expired, a quota ran out, or something inside got stuck.&lt;/p&gt;

&lt;p&gt;Restart-if-dead never fires, because nothing died.&lt;/p&gt;

&lt;p&gt;The fix is to watch &lt;strong&gt;work&lt;/strong&gt;, not &lt;strong&gt;liveness&lt;/strong&gt;. Have the bot write a timestamp every time it actually handles something, and have your script kill the process if that timestamp gets too old. Killing it is enough — the structure above brings it straight back.&lt;/p&gt;

&lt;p&gt;We got burned here in a way worth repeating. Our watchdog checked connectivity by fetching a page, and that fetch pulled the whole 170KB body. On a slow day it blew past the timeout, so &lt;strong&gt;the watchdog itself failed for eight hours&lt;/strong&gt; while the bot sat dead. Switching to a HEAD request brought it to 0.5 seconds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If the thing doing the watching is heavy, the watching breaks first.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Checking on it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;screen &lt;span class="nt"&gt;-ls&lt;/span&gt;
launchctl list | &lt;span class="nb"&gt;grep &lt;/span&gt;mybot
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;First line tells you the session exists, second that autostart is registered.&lt;/p&gt;

&lt;p&gt;One warning: &lt;code&gt;screen -X hardcopy&lt;/code&gt;, which dumps the screen to a file, &lt;strong&gt;produces an empty file for anything that draws a text UI.&lt;/strong&gt; We tried to use it for health checks and wasted time. Check for a child process and an established network connection instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Making the bot in the first place: Discord bot setup, from token to server invite&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;When it is alive but silent: Discord bot not responding but still running: how to catch it&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every command and number here comes from a machine that is running right now.&lt;/p&gt;

&lt;h3&gt;
  
  
  Related posts
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Discord bot not responding but still running: how to catch it&lt;/li&gt;
&lt;li&gt;npm command not found on Windows: fix the PATH&lt;/li&gt;
&lt;li&gt;codex CLI login not working on macOS: what fixed it&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://aisideincomelab.blogspot.com/2026/09/run-discord-cli-bot-247-on-macos-with.html" rel="noopener noreferrer"&gt;Homelab Notes&lt;/a&gt; — notes from one Mac mini running local LLMs and 24/7 automation.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>automation</category>
      <category>clitools</category>
      <category>macos</category>
    </item>
    <item>
      <title>Discord bot not responding but still running: how to catch it</title>
      <dc:creator>DevLog</dc:creator>
      <pubDate>Sun, 27 Sep 2026 06:43:17 +0000</pubDate>
      <link>https://dev.to/devlog/discord-bot-not-responding-but-still-running-how-to-catch-it-1ekb</link>
      <guid>https://dev.to/devlog/discord-bot-not-responding-but-still-running-how-to-catch-it-1ekb</guid>
      <description>&lt;p&gt;Your bot shows as online in Discord. The process is there in &lt;code&gt;ps&lt;/code&gt;. And it answers nothing.&lt;/p&gt;

&lt;p&gt;This is the failure that gets past every restart-if-it-dies setup, because &lt;strong&gt;nothing died.&lt;/strong&gt; Here is how to detect it and what to do about it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why your supervisor misses this
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;launchd&lt;/code&gt; with &lt;code&gt;KeepAlive&lt;/code&gt;, &lt;code&gt;systemd&lt;/code&gt; with &lt;code&gt;Restart=always&lt;/code&gt;, &lt;code&gt;pm2&lt;/code&gt;, a &lt;code&gt;while&lt;/code&gt; loop that checks for the process — they all watch the same thing. &lt;strong&gt;Is the process there?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When a session expires, a daily quota runs out, or a request hangs on a socket with no timeout, the process is still there. It holds memory, it shows up in the process list, and the supervisor is satisfied. The bot is dead in every way that matters and nothing will restart it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Signals that lie to you
&lt;/h2&gt;

&lt;p&gt;Before building a check, know which signals are useless here.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Discord shows the bot online. The gateway connection can outlive the part of your code that handles messages&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The process exists. That is exactly the thing that is wrong&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;launchctl list&lt;/code&gt; shows a PID. Same problem&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Logs are quiet. A stuck bot usually stops logging, and "no errors" reads like "no problems"&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A screen dump is empty. If your bot draws a text interface, &lt;code&gt;screen -X hardcopy&lt;/code&gt; writes an empty file. We tried to use it for health checks and wasted time on it&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Watch work, not life
&lt;/h2&gt;

&lt;p&gt;The check that works is the boring one. &lt;strong&gt;Have the bot record when it last did something real.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Not when it started. Not when it received a message. &lt;strong&gt;When it finished handling one.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;

&lt;span class="n"&gt;HEARTBEAT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/tmp/mybot.alive&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;on_message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;                      &lt;span class="c1"&gt;# handle the command
&lt;/span&gt;    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reply&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;HEARTBEAT&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;())))&lt;/span&gt;   &lt;span class="c1"&gt;# only after a real reply
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then a separate script decides whether that timestamp is too old.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;now&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%s&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;last&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; /tmp/mybot.alive 2&amp;gt;/dev/null &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;echo &lt;/span&gt;0&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="k"&gt;$((&lt;/span&gt;now &lt;span class="o"&gt;-&lt;/span&gt; last&lt;span class="k"&gt;))&lt;/span&gt; &lt;span class="nt"&gt;-gt&lt;/span&gt; 900 &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;pkill &lt;span class="nt"&gt;-f&lt;/span&gt; mybot.py          &lt;span class="c"&gt;# let the supervisor bring it back&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Killing it is the whole fix.&lt;/strong&gt; You do not need restart logic — whatever already restarts a dead process will handle it. You are just converting an invisible failure into a visible one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choosing the threshold
&lt;/h2&gt;

&lt;p&gt;Too short and you restart a bot that is merely idle. Too long and you are down for hours.&lt;/p&gt;

&lt;p&gt;The number depends on traffic, not on taste. If your bot handles something every few minutes, fifteen minutes is generous. If it goes quiet overnight, a timestamp alone will produce false restarts — in that case have the bot do a cheap internal task on a timer and stamp that, so the heartbeat reflects the bot being &lt;strong&gt;capable&lt;/strong&gt;, not being &lt;strong&gt;used&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep the watcher light
&lt;/h2&gt;

&lt;p&gt;This one cost us eight hours.&lt;/p&gt;

&lt;p&gt;Our watchdog checked connectivity before restarting the bot, and that check fetched a page — the whole 170KB body. On a slow connection it exceeded its own timeout, decided the network was down, and refused to restart. &lt;strong&gt;The bot was dead the entire time and the watchdog was the reason it stayed dead.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Switching to a HEAD request took it to 0.5 seconds.&lt;/p&gt;

&lt;p&gt;If a watchdog can fail, it will fail at the worst time. Keep every check in it cheap and give each one a timeout shorter than the loop interval.&lt;/p&gt;

&lt;h2&gt;
  
  
  A quick audit
&lt;/h2&gt;

&lt;p&gt;Answer these about your own setup:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;If the process froze right now, &lt;strong&gt;what would notice?&lt;/strong&gt; If the answer is "me, eventually", you have this problem&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Does your health check look at anything other than the process existing?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Can your watchdog itself hang? What is its slowest operation?&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Related
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;The setup this runs on: Run a Discord bot 24/7 for free on a computer you already own&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Environment traps when starting from a supervisor: mac environment variables that autostart does not have&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every number here is from a bot that has been running on the same machine for 34 days.&lt;/p&gt;

&lt;h3&gt;
  
  
  Related posts
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Run a Discord bot 24/7 for free on a computer you already own&lt;/li&gt;
&lt;li&gt;codex CLI login not working on macOS: what fixed it&lt;/li&gt;
&lt;li&gt;Claude CLI 401 Unauthorized Refresh Token Issue&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://aisideincomelab.blogspot.com/2026/09/catching-unresponsive-bots-launchd.html" rel="noopener noreferrer"&gt;Homelab Notes&lt;/a&gt; — notes from one Mac mini running local LLMs and 24/7 automation.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>automation</category>
      <category>localllm</category>
      <category>macos</category>
    </item>
    <item>
      <title>Getting started with uv: an existing project, a new project, and the numbers (pip 2.46 s uv 0.38 s)</title>
      <dc:creator>DevLog</dc:creator>
      <pubDate>Sat, 26 Sep 2026 09:43:13 +0000</pubDate>
      <link>https://dev.to/devlog/getting-started-with-uv-an-existing-project-a-new-project-and-the-numbers-pip-246-s-uv-038-41gl</link>
      <guid>https://dev.to/devlog/getting-started-with-uv-an-existing-project-a-new-project-and-the-numbers-pip-246-s-uv-038-41gl</guid>
      <description>&lt;p&gt;uv is a fast Python package and project manager written in Rust. This is the write-up of the video above: dropping uv into a project that already has a &lt;code&gt;requirements.txt&lt;/code&gt;, starting a new project from scratch, and a pip-versus-uv benchmark on one machine with the conditions stated. Every number below is on screen in the video and came from the same runs.&lt;/p&gt;

&lt;h2&gt;
  
  
  The two commands first
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Existing project: &lt;code&gt;uv pip install -r requirements.txt&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;New project: &lt;code&gt;uv init &amp;amp;&amp;amp; uv add &amp;amp;&amp;amp; uv sync&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If all you want is faster installs, the first line is the whole migration. The rest of this post is what happens around those two lines, and where to be careful.&lt;/p&gt;

&lt;h2&gt;
  
  
  Install
&lt;/h2&gt;

&lt;p&gt;One command, one binary. You do not need a Python on the machine to install it, which matters when the thing you are installing is what will manage your Pythons.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-LsSf&lt;/span&gt; https://astral.sh/uv/install.sh | sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The video pins version 0.11.6 and installs into a throwaway directory instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;curl &lt;span class="nt"&gt;-LsSf&lt;/span&gt; https://astral.sh/uv/0.11.6/install.sh | &lt;span class="nv"&gt;UV_INSTALL_DIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/tmp/uvtest sh
&lt;span class="go"&gt;downloading uv 0.11.6 aarch64-apple-darwin
installing to /tmp/uvtest
  uv
  uvx
everything's installed!
WARN: The following commands are shadowed by other commands in your PATH: uv uvx
&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;/tmp/uvtest/uv &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;span class="go"&gt;uv 0.11.6 (65950801c 2026-04-09 aarch64-apple-darwin)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why pin an old version? The benchmark ran on 0.11.6, and I wanted every number and every version string on screen to come from one build. Newer releases are faster in places, so read the numbers here as a floor. The "shadowed" warning appears because this Mac already had a uv on the PATH; you will not see it on a clean machine. For your own install, use the first command and take the latest.&lt;/p&gt;

&lt;h2&gt;
  
  
  Replace pip in a project you already have
&lt;/h2&gt;

&lt;p&gt;Nothing gets converted. uv ships a pip-compatible interface, so the command you already know still works against the same &lt;code&gt;requirements.txt&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ cat requirements.txt
requests
rich
httpx
pydantic
click
jinja2
pyyaml
python-dateutil
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same file installed twice, both with caches turned off:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;python3 &lt;span class="nt"&gt;-m&lt;/span&gt; venv .venv-pip &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;time&lt;/span&gt; .venv-pip/bin/pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="nt"&gt;--no-cache-dir&lt;/span&gt; &lt;span class="nt"&gt;--disable-pip-version-check&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt
&lt;span class="go"&gt;
real    0m2.643s
user    0m1.582s
sys     0m0.303s
&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;uv venv &lt;span class="nt"&gt;--python&lt;/span&gt; 3.14 .venv-uv &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;time &lt;/span&gt;uv pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="nt"&gt;--no-cache&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt &lt;span class="nt"&gt;--python&lt;/span&gt; .venv-uv/bin/python
&lt;span class="go"&gt;Using CPython 3.14.6 interpreter at: /opt/homebrew/opt/python@3.14/bin/python3.14
Creating virtual environment at: .venv-uv
Activate with: source .venv-uv/bin/activate

real    0m0.363s
user    0m0.153s
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice what did not happen: no rewritten requirements file, no new project layout, no CI changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The numbers, with the conditions
&lt;/h2&gt;

&lt;p&gt;Numbers without conditions are not worth much, so here they are together. Eight packages, caches disabled on both sides, five runs each, median.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Mac mini M4 Pro / macOS 26.6.1 / Python 3.14.6 / pip 26.1.2 / uv 0.11.6
8 packages, caches disabled on both sides, 5 runs each, median

  install       pip  2.457s     uv  0.381s      6.4x
  create venv   venv 1.143s     uv  0.026s     43.9x
  ------------------------------------------------------
  full flow     3.600s   -&amp;gt;   0.407s            8.8x

  (bonus) uv install with warm cache: 0.062s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things worth separating. Installing the packages is the headline, 6.4×. Creating the environment is a separate cost, and that gap is bigger: 1.143 s against 0.026 s. Put together, the thing you actually do went from 3.6 s to 0.4 s.&lt;/p&gt;

&lt;p&gt;One caveat: that is a cold install. With a warm cache uv drops to 0.062 s, but comparing a warm run against a cold one would be cheating, so treat that line as a bonus, not the headline.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Update, 2026-09-09: uv 0.12.11 shipped the day before this post went live&lt;/strong&gt;, so I re-ran the same eight packages, caches off, five runs each, median, on the same machine with both uv versions and pip 26.1.2 back to back:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;              create venv   install    full flow
pip 26.1.2       1.202s      2.338s      3.540s
uv 0.11.6        0.150s      0.383s      0.533s
uv 0.12.11       0.136s      0.375s      0.511s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Install is 6.2× faster than pip on 0.12.11 and the full flow 6.9×, so the video's numbers still hold as a floor. The two uv versions are within 4% of each other on this workload. The venv column differs from the table above because this re-run timed &lt;code&gt;uv venv --python 3.14&lt;/code&gt; from a Python harness rather than the shell's &lt;code&gt;time&lt;/code&gt;; compare rows within one table, not across the two.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start a new project: init, add, sync
&lt;/h2&gt;

&lt;p&gt;The other entry point is starting fresh. &lt;code&gt;uv init&lt;/code&gt; lays out a project, &lt;code&gt;uv add&lt;/code&gt; resolves and installs in one step while writing a lock file, and &lt;code&gt;uv sync&lt;/code&gt; rebuilds the environment from that lock file.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;uv init demo &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;demo &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;ls&lt;/span&gt;
&lt;span class="go"&gt;Initialized project `demo` at `.../uvdemo/demo`
main.py         pyproject.toml  README.md
&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;uv add requests rich
&lt;span class="go"&gt;Using CPython 3.13.13
Creating virtual environment at: .venv
Resolved 10 packages in 71ms
Prepared 7 packages in 79ms
Installed 9 packages in 10ms
 + certifi==2026.7.22
 + charset-normalizer==3.5.1
 + idna==3.19
 + markdown-it-py==4.2.0
 + mdurl==0.1.2
 + pygments==2.21.0
 + requests==2.34.2
 + rich==15.0.0
 + urllib3==2.7.0
&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;cat &lt;/span&gt;pyproject.toml
&lt;span class="go"&gt;[project]
name = "demo"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
&lt;/span&gt;&lt;span class="gp"&gt;requires-python = "&amp;gt;&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;3.13&lt;span class="s2"&gt;"
&lt;/span&gt;&lt;span class="go"&gt;dependencies = [
&lt;/span&gt;&lt;span class="gp"&gt;    "requests&amp;gt;&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;2.34.2&lt;span class="s2"&gt;",
&lt;/span&gt;&lt;span class="gp"&gt;    "rich&amp;gt;&lt;/span&gt;&lt;span class="s2"&gt;=15.0.0"&lt;/span&gt;,
&lt;span class="go"&gt;]
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is why the lock file exists. Delete the environment completely, run &lt;code&gt;uv sync&lt;/code&gt;, and you get the exact same versions back, not whatever resolves today:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; .venv &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; uv &lt;span class="nb"&gt;sync&lt;/span&gt;
&lt;span class="go"&gt;Using CPython 3.13.13
Creating virtual environment at: .venv
Resolved 10 packages in 0.59ms
Installed 9 packages in 7ms
 + certifi==2026.7.22
 + charset-normalizer==3.5.1
 + idna==3.19
 + markdown-it-py==4.2.0
 + mdurl==0.1.2
 + pygments==2.21.0
 + requests==2.34.2
 + rich==15.0.0
&lt;/span&gt;&lt;span class="c"&gt; ...
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;pyproject.toml&lt;/code&gt; says what the project asks for. &lt;code&gt;uv.lock&lt;/code&gt; records the exact resolved graph, transitive packages included. Commit it, and a teammate who clones the repo runs one command and has your environment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Run without activating
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;uv run&lt;/code&gt; resolves the project's environment, makes sure it is in sync, and runs your command inside it. No &lt;code&gt;source .venv/bin/activate&lt;/code&gt;, no shell state to lose.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;uv run python &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s1"&gt;'import requests; print(requests.__version__)'&lt;/span&gt;
&lt;span class="go"&gt;2.34.2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The interpreter it used is the project's own, not the system one, without touching anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  Python versions and tools
&lt;/h2&gt;

&lt;p&gt;uv installs Python itself. Pin a version per project and anyone who runs &lt;code&gt;uv sync&lt;/code&gt; gets that interpreter downloaded automatically. No pyenv, no system-Python roulette.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;uv python list | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-6&lt;/span&gt;
&lt;span class="go"&gt;cpython-3.15.0a8-macos-aarch64-none
cpython-3.15.0a8+freethreaded-macos-aarch64-none
&lt;/span&gt;&lt;span class="gp"&gt;cpython-3.14.6-macos-aarch64-none                   /opt/homebrew/bin/python3.14 -&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;...
&lt;span class="gp"&gt;cpython-3.14.6-macos-aarch64-none                   /opt/homebrew/bin/python3 -&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;...
&lt;span class="go"&gt;cpython-3.14.4-macos-aarch64-none
cpython-3.14.4+freethreaded-macos-aarch64-none
&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;uv run &lt;span class="nt"&gt;--python&lt;/span&gt; 3.13 python &lt;span class="nt"&gt;-V&lt;/span&gt;
&lt;span class="go"&gt;Python 3.13.13
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It also manages command-line tools. &lt;code&gt;uv tool install&lt;/code&gt; puts a command on your PATH for good; &lt;code&gt;uvx&lt;/code&gt; runs one once in a throwaway environment. Same package, different commitment.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uv tool &lt;span class="nb"&gt;install &lt;/span&gt;ruff
uvx ruff check &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Three things to check before moving a real project
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. The tool directory may not be on your PATH.&lt;/strong&gt; This happened while making the video. &lt;code&gt;uv tool install httpie&lt;/code&gt; succeeded, installed 16 packages and 3 executables, and printed the warning right there:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;uv tool &lt;span class="nb"&gt;install &lt;/span&gt;httpie
&lt;span class="go"&gt;Resolved 16 packages in 71ms
Prepared 2 packages in 23ms
Installed 16 packages in 25ms
&lt;/span&gt;&lt;span class="c"&gt; ...
&lt;/span&gt;&lt;span class="go"&gt;Installed 3 executables: http, httpie, https
&lt;/span&gt;&lt;span class="gp"&gt;warning: `/Users/smkim/.local/bin` is not on your PATH. To use installed tools, run `export PATH="/Users/smkim/.local/bin:$&lt;/span&gt;PATH&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt; or &lt;span class="sb"&gt;`&lt;/span&gt;&lt;span class="s2"&gt;uv tool update-shell&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;http &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;span class="go"&gt;bash: http: command not found
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;uv did warn. Until the PATH was fixed, the command still would not resolve. Read the last line of the install output.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Switching installs is cheap. Switching your team's workflow and CI at the same time is not.&lt;/strong&gt; The pip-compatible path exists so you can do the first without the second. Take the faster installs today; move the layout, lock file and CI when the team is ready.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Private indexes, authenticated registries and unusual build backends&lt;/strong&gt; are where a drop-in replacement stops being drop-in. It is not that uv cannot do them. It is that "it just works" stops being a safe assumption there, so test before you commit.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to do today
&lt;/h2&gt;

&lt;p&gt;Point uv at whatever you are working on right now and see what your install time drops to. That is the whole test.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uv pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Measured on a Mac mini M4 Pro, macOS 26.6.1, Python 3.14.6, pip 26.1.2, uv 0.11.6. The two timings in the "Replace pip" section are single runs from the recording; the table reports five-run medians. They are different runs on purpose, and they agree.&lt;/p&gt;

&lt;h3&gt;
  
  
  Related posts
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Why your Python script is slow: two lines took it 3.71 s to 1.20 s&lt;/li&gt;
&lt;li&gt;Run a Discord bot 24/7 for free on a computer you already own&lt;/li&gt;
&lt;li&gt;codex CLI login not working on macOS: what fixed it&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://aisideincomelab.blogspot.com/2026/09/getting-started-with-uv-existing.html" rel="noopener noreferrer"&gt;Homelab Notes&lt;/a&gt; — notes from one Mac mini running local LLMs and 24/7 automation.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>clitools</category>
      <category>videonotes</category>
    </item>
    <item>
      <title>ffmpeg: Convert video to optimized GIF on macOS</title>
      <dc:creator>DevLog</dc:creator>
      <pubDate>Sat, 26 Sep 2026 06:43:12 +0000</pubDate>
      <link>https://dev.to/devlog/ffmpeg-convert-video-to-optimized-gif-on-macos-55jd</link>
      <guid>https://dev.to/devlog/ffmpeg-convert-video-to-optimized-gif-on-macos-55jd</guid>
      <description>&lt;p&gt;Whenever I need to turn a video into a GIF, most search results lead me to upload-based websites. These usually have strict file size or length limits, and I prefer not to upload my raw files to a third-party server. Since I use a Mac mini with Apple Silicon, I wanted a way to do this locally.&lt;/p&gt;

&lt;p&gt;To get started, you need &lt;code&gt;Homebrew&lt;/code&gt; (a package manager for macOS). If you don't have it, install it via the terminal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;/bin/bash &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify the installation with &lt;code&gt;brew --version&lt;/code&gt;. Once that is sorted, install &lt;code&gt;ffmpeg&lt;/code&gt;, a powerful tool for handling almost any video or audio task:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;brew &lt;span class="nb"&gt;install &lt;/span&gt;ffmpeg
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run &lt;code&gt;ffmpeg -version&lt;/code&gt; to confirm it's working. On my M1 Mac mini, the installation took less than a minute.&lt;/p&gt;

&lt;p&gt;My first attempt was to keep it simple. I found a basic one-line command and tried it on a 15-second Full HD (1920x1080) video:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ffmpeg &lt;span class="nt"&gt;-i&lt;/span&gt; input.mp4 output_single_pass.gif
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The result was a "capacity bomb." The GIF ended up being 25.1 MB, even though the source was just a simple smartphone clip. While usable for very short clips, the file size becomes impossible to share for anything longer.&lt;/p&gt;

&lt;p&gt;The issue is that GIFs only support 256 colors. Trying to preserve every color from a high-resolution video causes the file size to explode. To fix this, you must first generate a custom color palette.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using a two-step conversion for optimization
&lt;/h2&gt;

&lt;p&gt;To dramatically reduce the size, you need to extract an optimized color palette from the source video and then use that palette to render the GIF.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Generate the palette&lt;/strong&gt;&lt;br&gt;
First, extract the best 256 colors from &lt;code&gt;input.mp4&lt;/code&gt; and save them as &lt;code&gt;palette.png&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ffmpeg &lt;span class="nt"&gt;-i&lt;/span&gt; input.mp4 &lt;span class="nt"&gt;-vf&lt;/span&gt; &lt;span class="s2"&gt;"fps=10,scale=480:-1:flags=lanczos,palettegen"&lt;/span&gt; palette.png
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here is what the &lt;code&gt;-vf&lt;/code&gt; (video filter) options are doing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;fps=10&lt;/code&gt;: Sets the frames per second to 10. Standard video is usually 24–30 fps, but 10–15 is plenty for a GIF and saves significant space.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;scale=480:-1&lt;/code&gt;: Resizes the width to 480 pixels. The &lt;code&gt;-1&lt;/code&gt; tells ffmpeg to calculate the height automatically to maintain the aspect ratio.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;flags=lanczos&lt;/code&gt;: A high-quality scaling algorithm that minimizes quality loss during resizing.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;palettegen&lt;/code&gt;: The filter that creates the optimized 256-color palette.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On my 15-second clip, this step took about 4 seconds to produce &lt;code&gt;palette.png&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2: Convert using the palette&lt;/strong&gt;&lt;br&gt;
Now, combine the original video and the palette to create the final GIF.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ffmpeg &lt;span class="nt"&gt;-i&lt;/span&gt; input.mp4 &lt;span class="nt"&gt;-i&lt;/span&gt; palette.png &lt;span class="nt"&gt;-filter_complex&lt;/span&gt; &lt;span class="s2"&gt;"fps=10,scale=480:-1:flags=lanczos[x];[x][1:v]paletteuse"&lt;/span&gt; output_optimized.gif
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this command, &lt;code&gt;-filter_complex&lt;/code&gt; is used because there are two inputs. The video stream &lt;code&gt;[x]&lt;/code&gt; (processed with the same fps and scale) is combined with the palette stream &lt;code&gt;[1:v]&lt;/code&gt; using the &lt;code&gt;paletteuse&lt;/code&gt; filter. This final step took about 5 seconds.&lt;/p&gt;

&lt;h3&gt;
  
  
  Performance Comparison
&lt;/h3&gt;

&lt;p&gt;To see the actual difference, I tested both methods using the same 15-second 1080p MP4 (original size: 8.7 MB).&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;File Size&lt;/th&gt;
&lt;th&gt;Conversion Time&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Single-pass&lt;/td&gt;
&lt;td&gt;25.1 MB&lt;/td&gt;
&lt;td&gt;3.2s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Two-step (Optimized)&lt;/td&gt;
&lt;td&gt;2.5 MB&lt;/td&gt;
&lt;td&gt;9.0s (3.8s palette + 5.2s GIF)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The optimized version is over 10 times smaller than the single-pass version. While it takes about three times longer to process, that is a small price to pay for such a massive reduction in file size.&lt;/p&gt;

&lt;p&gt;A few things to keep in mind: &lt;code&gt;fps&lt;/code&gt; is critical; pushing it above 20 will cause your file size to skyrocket. Also, reducing the &lt;code&gt;scale&lt;/code&gt; is the most effective way to cut size—dropping from 960px to 480px reduces total pixels by a factor of four. I once tried to turn a full hour of footage into a GIF, and my Mac mini fans started screaming; keep your clips short for the best results.&lt;/p&gt;

&lt;p&gt;It was a good reminder that simply finding a command that "works" isn't enough. Taking the time to measure results and understand the underlying logic—like how the GIF color palette works—saves a lot of frustration in the long run.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://aisideincomelab.blogspot.com/2026/09/ffmpeg-convert-video-to-optimized-gif.html" rel="noopener noreferrer"&gt;Homelab Notes&lt;/a&gt; — notes from one Mac mini running local LLMs and 24/7 automation.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>macos</category>
      <category>clitools</category>
      <category>troubleshooting</category>
    </item>
    <item>
      <title>Why your Python script is slow: two lines took it 3.71 s to 1.20 s</title>
      <dc:creator>DevLog</dc:creator>
      <pubDate>Sat, 26 Sep 2026 06:43:11 +0000</pubDate>
      <link>https://dev.to/devlog/why-your-python-script-is-slow-two-lines-took-it-371-s-to-120-s-1g85</link>
      <guid>https://dev.to/devlog/why-your-python-script-is-slow-two-lines-took-it-371-s-to-120-s-1g85</guid>
      <description>&lt;p&gt;One script, one million rows, one question: where does the time go? This is the write-up of the video above. Every number comes from the same measurement files the video used, all on one Mac mini (M4 Pro, 48 GB) with Python 3.14.6. The workload is synthetic: a deterministic generator (seed 42) writes a sales CSV so anyone can reproduce the run.&lt;/p&gt;

&lt;h2&gt;
  
  
  The result first
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;report.py (baseline): 3.71 s — single run, timed inside the script&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;report_cached.py (two lines added): 1.20 s — single run, timed inside the script&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;cmp report.txt report2.txt&lt;/code&gt; → identical, byte for byte&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;About 3.1× faster, same output, two lines changed. The interesting part is how we found the two lines, and where the trick stops working.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the script does
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;report.py&lt;/code&gt; reads a CSV of 1,000,000 sales rows, parses each row's date string, totals revenue by month and region, and writes a text report. We measure wall time, and the report must stay identical.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;python3 report.py sales.csv report.txt
&lt;span class="go"&gt;wrote report.txt: 1,000,000 rows in 3.71 s
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Not slow enough to notice once. Slow enough to hate inside a loop.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ask Python where the time goes
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;python3 &lt;span class="nt"&gt;-m&lt;/span&gt; cProfile &lt;span class="nt"&gt;-s&lt;/span&gt; tottime report.py sales.csv report.txt | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-12&lt;/span&gt;
&lt;span class="go"&gt;         32016593 function calls (32016422 primitive calls) in 8.440 seconds

   Ordered by: internal time

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
  1000000    3.045    0.000    5.556    0.000 _strptime.py:516(_strptime)
  1000001    0.859    0.000    1.411    0.000 csv.py:173(__next__)
  1000000    0.465    0.000    6.021    0.000 _strptime.py:812(_strptime_datetime_datetime)
        1    0.451    0.451    6.947    6.947 report.py:12(summarize)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The top line is the date parser, &lt;code&gt;strptime&lt;/code&gt;: one million calls, 3.045 s of self-time, 36% of the profiled run. Two cautions that the profiler itself teaches: self-time excludes subcalls, and profiling changes the timing (8.44 s here versus 3.71 s unprofiled). Read the share, not the seconds. The share points at parsing.&lt;/p&gt;

&lt;h2&gt;
  
  
  A profile says which function, not why
&lt;/h2&gt;

&lt;p&gt;So look at the input.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;python3 count_distinct.py sales.csv
&lt;span class="go"&gt;1,000,000 rows, 365 distinct dates
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One million calls, 365 distinct strings. The same date string gets parsed about 2,740 times.&lt;/p&gt;

&lt;h2&gt;
  
  
  The two-line change
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt; from collections import defaultdict
&lt;span class="gi"&gt;+from functools import lru_cache
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="gi"&gt;+@lru_cache(maxsize=None)
&lt;/span&gt; def parse_date(s):
     return datetime.strptime(s, "%Y-%m-%d %H:%M:%S")
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Prediction before running it: &lt;code&gt;strptime&lt;/code&gt; executes 365 times instead of 1,000,000, and the report does not change.&lt;/p&gt;

&lt;h2&gt;
  
  
  Did the prediction hold?
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;python3 report_cached.py sales.csv report2.txt
&lt;span class="go"&gt;wrote report2.txt: 1,000,000 rows in 1.20 s
&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;cmp report.txt report2.txt &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo &lt;/span&gt;identical
&lt;span class="go"&gt;identical
&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;python3 cache_check.py sales.csv
&lt;span class="go"&gt;CacheInfo(hits=999635, misses=365, maxsize=None, currsize=365)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;365 misses, 999,635 hits. The cache holds exactly 365 entries.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it works
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;strptime&lt;/code&gt; is expensive; a dictionary lookup is cheap. With 365 distinct inputs the cache misses 365 times and hits everywhere else. Which raises the real test: what if every date is different?&lt;/p&gt;

&lt;h2&gt;
  
  
  The limit: when inputs don't repeat
&lt;/h2&gt;

&lt;p&gt;Three data sets, five runs per version, medians. These are whole-process timings (startup and shutdown included), which is why they sit above the single in-script runs at the top. Compare plain and cached within a row, not across the two timers.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ python3 bench.py --table
distinct dates    plain   cached  speedup  same output
           365    3.96s    1.43s    2.77x  yes
        20,000    3.70s    1.49s    2.48x  yes
     1,000,000    3.76s    3.98s    0.95x  yes
1,000,000 rows · 5 runs each, median · Python 3.14.6
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, caching paid when inputs repeated. With every row unique, the cache cost about 6% and returned nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to use it
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Try caching an expensive pure function when its inputs repeat.&lt;/strong&gt; Count the inputs before you trust the speedup.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Use when: the same strings hit a parser thousands of times; the function is pure (same input, same output); you counted, and distinct inputs ≪ calls.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Doesn't fix: unique inputs (here, about 6% slower); loading — a separate benchmark took about 0.9 s just to read the CSV and build the rows; memory — the cache keeps every distinct key.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Start by profiling your script and inspecting its most expensive functions. Check their inputs, then benchmark any change without the profiler:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 &lt;span class="nt"&gt;-m&lt;/span&gt; cProfile &lt;span class="nt"&gt;-s&lt;/span&gt; tottime your_script.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Reproduce it
&lt;/h2&gt;

&lt;p&gt;Code, data generator and every capture: in the pinned comment under the video&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 make_data.py &lt;span class="nt"&gt;--rows&lt;/span&gt; 1000000 &lt;span class="nt"&gt;--unique&lt;/span&gt; 365 &lt;span class="nt"&gt;--out&lt;/span&gt; sales.csv
python3 report.py sales.csv report.txt
python3 &lt;span class="nt"&gt;-m&lt;/span&gt; cProfile &lt;span class="nt"&gt;-s&lt;/span&gt; tottime report.py sales.csv report.txt | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-12&lt;/span&gt;
python3 count_distinct.py sales.csv
python3 report_cached.py sales.csv report2.txt &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; cmp report.txt report2.txt &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo &lt;/span&gt;identical
python3 cache_check.py sales.csv
python3 bench.py &lt;span class="nt"&gt;--table&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two timing scopes appear above on purpose. The 3.71 s and 1.20 s are single measurements timed inside the script. The table reports five-run median whole-process times. They are not the same timer, and the ratios are what transfer.&lt;/p&gt;

&lt;h3&gt;
  
  
  Related posts
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Run a Discord bot 24/7 for free on a computer you already own&lt;/li&gt;
&lt;li&gt;codex CLI login not working on macOS: what fixed it&lt;/li&gt;
&lt;li&gt;Discord bot not responding but still running: how to catch it&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://aisideincomelab.blogspot.com/2026/09/what-actually-makes-this-python-script.html" rel="noopener noreferrer"&gt;Homelab Notes&lt;/a&gt; — notes from one Mac mini running local LLMs and 24/7 automation.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>troubleshooting</category>
      <category>videonotes</category>
    </item>
    <item>
      <title>10 Python tricks you'll actually use, 8 of them with zero installs (with the numbers)</title>
      <dc:creator>DevLog</dc:creator>
      <pubDate>Fri, 25 Sep 2026 12:43:08 +0000</pubDate>
      <link>https://dev.to/devlog/10-python-tricks-youll-actually-use-8-of-them-with-zero-installs-with-the-numbers-pkc</link>
      <guid>https://dev.to/devlog/10-python-tricks-youll-actually-use-8-of-them-with-zero-installs-with-the-numbers-pkc</guid>
      <description>&lt;p&gt;Ten Python tricks, shown live on one machine. Eight ship with Python itself; two are worth a &lt;code&gt;brew install&lt;/code&gt;. This is the write-up of the video above: every command, every output, and how each number was measured. Python 3.14.6 throughout.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. uv: the pip workflow, minus the wait
&lt;/h2&gt;

&lt;p&gt;One Rust binary that stands in for &lt;code&gt;pip&lt;/code&gt; and &lt;code&gt;venv&lt;/code&gt;, same workflow, familiar commands.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;brew &lt;span class="nb"&gt;install &lt;/span&gt;uv
uv venv &lt;span class="nt"&gt;-p&lt;/span&gt; 3.14
&lt;span class="nv"&gt;UV_NO_CACHE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1 uv pip &lt;span class="nb"&gt;install &lt;/span&gt;requests flask
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three cold installs, pip versus uv, on this machine:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cold install, pip vs uv

package        pip      uv   ratio
requests     0.86s   0.15s    5.7x
flask        0.94s   0.15s    6.4x
pandas       4.07s   0.59s    6.9x

python 3.14.6 both · caches off · 5 runs each, median · alternating order
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Conditions: cold installs from PyPI, caches off on both sides, five runs each, median, alternating order so neither tool benefits from going second. The installed artifacts were checked to be identical. uv 0.11.6. For the full setup, existing project and new project, see the uv video and its write-up on this blog.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. ruff: flake8, black and isort, retired
&lt;/h2&gt;

&lt;p&gt;One tool that lints and formats. Here it cold-scans a frozen snapshot of a real repo, 43 files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;brew &lt;span class="nb"&gt;install &lt;/span&gt;ruff
&lt;span class="nv"&gt;$ &lt;/span&gt;ruff check &lt;span class="nt"&gt;--no-cache&lt;/span&gt; &lt;span class="nt"&gt;--statistics&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt; | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-12&lt;/span&gt;
94      C408    &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; unnecessary-collection-call
84      SIM115  &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; open-file-with-context-handler
42      BLE001  &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; blind-except
25      PLW1510 &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; subprocess-run-without-check
22      RUF100  &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; unused-noqa
14      I001    &lt;span class="o"&gt;[&lt;/span&gt;-] unsorted-imports
12      FURB167 &lt;span class="o"&gt;[&lt;/span&gt;-] regex-flag-alias
12      S110    &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; try-except-pass
10      F401    &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; unused-import
 8      B023    &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt;&lt;span class="nt"&gt;-uses-loop-variable&lt;/span&gt;
 5      EXE001  &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; shebang-not-executable
 5      S112    &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; try-except-continue
&lt;span class="nv"&gt;$ &lt;/span&gt;ruff check &lt;span class="nt"&gt;--no-cache&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt; | &lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-2&lt;/span&gt;
Found 349 errors.
&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; 62 fixable with the &lt;span class="sb"&gt;`&lt;/span&gt;&lt;span class="nt"&gt;--fix&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt; option &lt;span class="o"&gt;(&lt;/span&gt;99 hidden fixes can be enabled with the &lt;span class="sb"&gt;`&lt;/span&gt;&lt;span class="nt"&gt;--unsafe-fixes&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt; option&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;349 issues across 43 files in 46 ms, one timed cold run with &lt;code&gt;--no-cache&lt;/code&gt;, ruff 0.16.5. &lt;code&gt;[*]&lt;/code&gt; marks what &lt;code&gt;--fix&lt;/code&gt; will handle for you.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. f'{x=}': stop typing it twice
&lt;/h2&gt;

&lt;p&gt;Put an equals sign inside the braces and Python prints the variable name and its value together. Built in since 3.8.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt;
&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;
&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;team&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;infra&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;team&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;team&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;infra&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;84&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expressions work too, as the second line shows.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. breakpoint(): a debugger was here all along
&lt;/h2&gt;

&lt;p&gt;Drop it on any line and you are inside a live debugger. No IDE, no setup. Built in since 3.7.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;cat &lt;/span&gt;app.py
&lt;span class="go"&gt;def score(user):
    base = user["points"] * 2
    breakpoint()
    return base + bonus(user)

def bonus(user):
    return 10 if user["team"] == "infra" else 0

print(score({"name": "mara", "team": "infra", "points": 41}))
&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;python3 app.py
&lt;span class="gp"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;/private/tmp/pydemo/app.py&lt;span class="o"&gt;(&lt;/span&gt;3&lt;span class="o"&gt;)&lt;/span&gt;score&lt;span class="o"&gt;()&lt;/span&gt;
&lt;span class="gp"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;breakpoint&lt;span class="o"&gt;()&lt;/span&gt;
&lt;span class="go"&gt;(Pdb) p user
{'name': 'mara', 'team': 'infra', 'points': 41}
(Pdb) p base
82
(Pdb) c
92
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;p&lt;/code&gt; prints, &lt;code&gt;n&lt;/code&gt; steps, &lt;code&gt;c&lt;/code&gt; continues. Inspect anything, step anywhere.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. python3 -i: crash autopsy, live
&lt;/h2&gt;

&lt;p&gt;Your script crashed. Run it again with &lt;code&gt;-i&lt;/code&gt; and Python drops you at a prompt with the script's variables still loaded.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;cat &lt;/span&gt;crash.py
&lt;span class="go"&gt;rows = [line.strip().split(",") for line in open("users.csv")]
header, data = rows[0], rows[1:]
total = sum(int(r[2]) for r in data)
print("total points:", total)
&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;python3 &lt;span class="nt"&gt;-i&lt;/span&gt; crash.py
&lt;span class="go"&gt;Traceback (most recent call last):
  File "/private/tmp/pydemo/crash.py", line 3, in
    total = sum(int(r[2]) for r in data)
  File "/private/tmp/pydemo/crash.py", line 3, in
    total = sum(int(r[2]) for r in data)
ValueError: invalid literal for int() with base 10: 'receipt-7'
&lt;/span&gt;&lt;span class="gp"&gt;&amp;gt;&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; data[1]
&lt;span class="go"&gt;['jon', 'web', 'receipt-7']
&lt;/span&gt;&lt;span class="gp"&gt;&amp;gt;&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; header
&lt;span class="go"&gt;['name', 'team', 'points']
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The traceback names the value. The prompt shows which row it came from. Do the autopsy right there.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. http.server: a web server in one line
&lt;/h2&gt;

&lt;p&gt;Zero installs, and the current folder is on your local network. Hand a file to the laptop next to you, then shut it down.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;python3 &lt;span class="nt"&gt;-m&lt;/span&gt; http.server 8000 2&amp;gt;/dev/null &amp;amp;
&lt;span class="go"&gt;[1] 77428
&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;curl &lt;span class="nt"&gt;-sI&lt;/span&gt; localhost:8000/orders.json | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-3&lt;/span&gt;
&lt;span class="go"&gt;HTTP/1.0 200 OK
Server: SimpleHTTP/0.6 Python/3.14.6
Date: Tue, 01 Sep 2026 14:59:55 GMT
&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; localhost:8000/orders.json | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; 80
&lt;span class="go"&gt;{"orders":[{"id":9174,"user":"mara","items":[{"sku":"KB-01","qty":2,"price":39.5
&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;kill&lt;/span&gt; %1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It serves everything in the directory to anyone on the network. Use it on a network you trust, and kill it when you are done.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. json.tool: jq, when there's no jq
&lt;/h2&gt;

&lt;p&gt;Any box with Python can pretty-print JSON straight from a pipe.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;cat&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;orders.json&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;python&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;-m&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;json.tool&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"orders"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;9174&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"user"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"mara"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"items"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
                    &lt;/span&gt;&lt;span class="nl"&gt;"sku"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"KB-01"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                    &lt;/span&gt;&lt;span class="nl"&gt;"qty"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                    &lt;/span&gt;&lt;span class="nl"&gt;"price"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;39.5&lt;/span&gt;&lt;span class="w"&gt;
                &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="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"shipped"&lt;/span&gt;&lt;span class="w"&gt;
        &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="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"count"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  8. timeit: measure, don't guess
&lt;/h2&gt;

&lt;p&gt;It runs your line enough times to trust the number and reports the best of five. Same line, two sizes; watch the cost move.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;python3 &lt;span class="nt"&gt;-m&lt;/span&gt; timeit &lt;span class="s1"&gt;'sum(range(1000))'&lt;/span&gt;
&lt;span class="go"&gt;100000 loops, best of 5: 3.7 usec per loop
&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;python3 &lt;span class="nt"&gt;-m&lt;/span&gt; timeit &lt;span class="s1"&gt;'sum(range(100000))'&lt;/span&gt;
&lt;span class="go"&gt;500 loops, best of 5: 425 usec per loop
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;timeit picks the loop count itself, which is why the first line ran 100,000 loops and the second 500. Those are the live runs from the recording.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. -X importtime: find out where startup goes
&lt;/h2&gt;

&lt;p&gt;Your script feels slow before it even starts? This flag breaks down where that time goes, import by import. Here it is on pandas, output saved with &lt;code&gt;tee&lt;/code&gt; so the numbers on screen are the numbers in the file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;python &lt;span class="nt"&gt;-X&lt;/span&gt; importtime &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s1"&gt;'import pandas'&lt;/span&gt; 2&amp;gt;&amp;amp;1 | &lt;span class="nb"&gt;tee &lt;/span&gt;it.log | &lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-6&lt;/span&gt;
&lt;span class="go"&gt;import time:       386 |        386 |     pandas.io.sql
import time:       145 |        145 |     pandas.io.xml
import time:       104 |       4290 |   pandas.io.api
import time:        73 |         73 |   pandas.util._tester
import time:        58 |         58 |   pandas._version_meson
import time:       392 |     187650 | pandas
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Columns are self time and cumulative time in microseconds. &lt;code&gt;import pandas&lt;/code&gt; cost 187,650 µs, about 187 ms, on this run. Sort the log by the second column to find the imports worth deferring.&lt;/p&gt;

&lt;h2&gt;
  
  
  10. python3 -u: your logs were there all along
&lt;/h2&gt;

&lt;p&gt;Python buffers &lt;code&gt;print&lt;/code&gt; output when there is no terminal on the other end, so logs run late and a crash can eat them. &lt;code&gt;-u&lt;/code&gt; turns buffering off and every line lands the moment it is written.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cat &lt;/span&gt;slow_logs.py
import &lt;span class="nb"&gt;time

&lt;/span&gt;&lt;span class="k"&gt;for &lt;/span&gt;step &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"load config"&lt;/span&gt;, &lt;span class="s2"&gt;"connect db"&lt;/span&gt;, &lt;span class="s2"&gt;"warm cache"&lt;/span&gt;, &lt;span class="s2"&gt;"start worker"&lt;/span&gt;, &lt;span class="s2"&gt;"ready"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;:
    print&lt;span class="o"&gt;(&lt;/span&gt;f&lt;span class="s2"&gt;"[boot] {step}"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    time.sleep&lt;span class="o"&gt;(&lt;/span&gt;0.8&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;python3 slow_logs.py | &lt;span class="nb"&gt;cat&lt;/span&gt;        &lt;span class="c"&gt;# all five lines appear at once, at the end&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;python3 &lt;span class="nt"&gt;-u&lt;/span&gt; slow_logs.py | &lt;span class="nb"&gt;cat&lt;/span&gt;     &lt;span class="c"&gt;# one line every 0.8 s, as they happen&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output text is identical; the timing is not. In a container or under a process manager, &lt;code&gt;-u&lt;/code&gt; or &lt;code&gt;PYTHONUNBUFFERED=1&lt;/code&gt; is the difference between seeing the last log line before a crash and not.&lt;/p&gt;

&lt;h2&gt;
  
  
  All ten commands
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;brew &lt;span class="nb"&gt;install &lt;/span&gt;uv
uv venv &lt;span class="nt"&gt;-p&lt;/span&gt; 3.14
uv pip &lt;span class="nb"&gt;install &lt;/span&gt;requests flask
brew &lt;span class="nb"&gt;install &lt;/span&gt;ruff
ruff check &lt;span class="nt"&gt;--no-cache&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;
print&lt;span class="o"&gt;(&lt;/span&gt;f&lt;span class="s1"&gt;'{x=}'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
breakpoint&lt;span class="o"&gt;()&lt;/span&gt;
python3 &lt;span class="nt"&gt;-i&lt;/span&gt; app.py
python3 &lt;span class="nt"&gt;-m&lt;/span&gt; http.server 8000
python3 &lt;span class="nt"&gt;-m&lt;/span&gt; json.tool
python3 &lt;span class="nt"&gt;-m&lt;/span&gt; timeit &lt;span class="s1"&gt;'sum(range(1000))'&lt;/span&gt;
python3 &lt;span class="nt"&gt;-X&lt;/span&gt; importtime app.py
python3 &lt;span class="nt"&gt;-u&lt;/span&gt; app.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;How things were measured, all on a Mac mini M4 Pro with Python 3.14.6: uv versus pip is three packages, cold installs from PyPI, caches off, five runs each, median, alternating order. ruff is one timed cold scan of a frozen snapshot of the repo. importtime is the exact run shown on screen. timeit outputs are the live runs; timeit picks the loop counts and reports the best of five.&lt;/p&gt;

&lt;h3&gt;
  
  
  Related posts
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Why your Python script is slow: two lines took it 3.71 s to 1.20 s&lt;/li&gt;
&lt;li&gt;Run a Discord bot 24/7 for free on a computer you already own&lt;/li&gt;
&lt;li&gt;npm command not found on Windows: fix the PATH&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://aisideincomelab.blogspot.com/2026/09/10-python-tricks-youll-actually-use-8.html" rel="noopener noreferrer"&gt;Homelab Notes&lt;/a&gt; — notes from one Mac mini running local LLMs and 24/7 automation.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>clitools</category>
      <category>videonotes</category>
    </item>
    <item>
      <title>Generate video subtitles for free with Whisper on a Mac</title>
      <dc:creator>DevLog</dc:creator>
      <pubDate>Fri, 25 Sep 2026 12:43:07 +0000</pubDate>
      <link>https://dev.to/devlog/generate-video-subtitles-for-free-with-whisper-on-a-mac-8ce</link>
      <guid>https://dev.to/devlog/generate-video-subtitles-for-free-with-whisper-on-a-mac-8ce</guid>
      <description>&lt;p&gt;Subtitling is the part of making a video that nobody enjoys. You listen, you type, you line up the timings, and it takes longer than the edit did.&lt;/p&gt;

&lt;p&gt;Whisper does it for you. It runs on your own machine, it costs nothing, and it does not need an internet connection once it is set up. You hand it a video file and you get a subtitle file back.&lt;/p&gt;

&lt;h2&gt;
  
  
  Install
&lt;/h2&gt;

&lt;p&gt;There are several ways to run Whisper. On a Mac, &lt;code&gt;whisper-ctranslate2&lt;/code&gt; is the least trouble — one command, and it is fast.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uv tool &lt;span class="nb"&gt;install &lt;/span&gt;whisper-ctranslate2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you are not using &lt;code&gt;uv&lt;/code&gt;, &lt;code&gt;pip install whisper-ctranslate2&lt;/code&gt; works the same way. &lt;code&gt;uv&lt;/code&gt; just keeps it out of your other Python environments.&lt;/p&gt;

&lt;h2&gt;
  
  
  First run
&lt;/h2&gt;

&lt;p&gt;Point it at the video. You do not need to extract the audio first.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;whisper-ctranslate2 &lt;span class="nt"&gt;--model&lt;/span&gt; large-v3-turbo &lt;span class="nt"&gt;--language&lt;/span&gt; ko &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--output_format&lt;/span&gt; srt video.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first run downloads the model — about 1.5GB. Ours took 52 seconds including that download. After that it runs offline.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Set &lt;code&gt;--language&lt;/code&gt; explicitly.&lt;/strong&gt; Left to guess, it reads the opening seconds, and a quiet intro can send it to the wrong language entirely.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you get
&lt;/h2&gt;

&lt;p&gt;An &lt;code&gt;.srt&lt;/code&gt; file appears next to your video. Plain text, opens in any editor.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1
00:00:00,000 --&amp;gt; 00:00:04,500
Text of the first line here.

2
00:00:05,120 --&amp;gt; 00:00:07,440
And the second.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That format uploads straight to YouTube and imports into every editor.&lt;/p&gt;

&lt;h2&gt;
  
  
  How long it takes
&lt;/h2&gt;

&lt;p&gt;Measured on a Mac mini:&lt;/p&gt;

&lt;p&gt;InputTime16.6 s of audio12 s12 s video clip12 sFirst run (incl. 1.5GB download)52 s&lt;br&gt;
&lt;strong&gt;Faster than real time.&lt;/strong&gt; A ten minute video finishes in under ten minutes. Start it and go do something else.&lt;/p&gt;
&lt;h2&gt;
  
  
  Do not drop to a smaller model
&lt;/h2&gt;

&lt;p&gt;The temptation is to pick &lt;code&gt;tiny&lt;/code&gt; or &lt;code&gt;base&lt;/code&gt; and go faster.&lt;/p&gt;

&lt;p&gt;For English you can get away with it. &lt;strong&gt;For most other languages you cannot.&lt;/strong&gt; We tested the small models on Korean and went straight back to the large one. Fixing the mistakes by hand costs far more time than the faster run saves.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;large-v3-turbo&lt;/code&gt; is the sensible default: it is the large model with the speed problem solved.&lt;/p&gt;
&lt;h2&gt;
  
  
  It is good, not perfect
&lt;/h2&gt;

&lt;p&gt;Here is a real line from our own output. The speaker said "봇들" (bots) and Whisper heard "못둘".&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[00:03.080 --&amp;gt; 00:07.000]  그 안에서 디스코드 못둘 로컬 LLM 영상 복원이 같이 돕니다.
                                        ^^^^
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The places it slips are predictable:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Invented words. Product names, tool names, acronyms&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Similar-sounding endings. The example above&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Numbers and units. There is usually more than one way to read them aloud&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Spacing. Rarely matters in a subtitle&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So &lt;strong&gt;read it once before you ship it.&lt;/strong&gt; But reading and fixing four lines is a different job from transcribing ten minutes from scratch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Another use for it
&lt;/h2&gt;

&lt;p&gt;It is not only for subtitles.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use it to check synthetic speech.&lt;/strong&gt; If you generate narration with a text-to-speech model, transcribe the result and diff it against your script. Anything it read wrong shows up immediately. We caught a homophone problem this way and fixed it in the script rather than fighting the audio.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Making that narration in the first place: Clone your own voice with a 17-second recording&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every command and number here was run on the machine this blog is written on.&lt;/p&gt;

&lt;h3&gt;
  
  
  Related posts
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Run a Discord bot 24/7 for free on a computer you already own&lt;/li&gt;
&lt;li&gt;Why your Python script is slow: two lines took it 3.71 s to 1.20 s&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://aisideincomelab.blogspot.com/2026/09/generate-video-subtitles-for-free-with.html" rel="noopener noreferrer"&gt;Homelab Notes&lt;/a&gt; — notes from one Mac mini running local LLMs and 24/7 automation.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>mac</category>
      <category>subtitles</category>
      <category>video</category>
      <category>whisper</category>
    </item>
  </channel>
</rss>
