<?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: Javi Palacios</title>
    <description>The latest articles on DEV Community by Javi Palacios (@fj_palacios).</description>
    <link>https://dev.to/fj_palacios</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%2F3958394%2F53300b47-6c71-4239-a646-2a8cc4b00d1e.jpeg</url>
      <title>DEV Community: Javi Palacios</title>
      <link>https://dev.to/fj_palacios</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/fj_palacios"/>
    <language>en</language>
    <item>
      <title>Dockerfile Best Practices: Security</title>
      <dc:creator>Javi Palacios</dc:creator>
      <pubDate>Tue, 25 Aug 2026 10:35:55 +0000</pubDate>
      <link>https://dev.to/fj_palacios/dockerfile-best-practices-security-2p5g</link>
      <guid>https://dev.to/fj_palacios/dockerfile-best-practices-security-2p5g</guid>
      <description>&lt;p&gt;Run this against your current containers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker inspect &lt;span class="si"&gt;$(&lt;/span&gt;docker ps &lt;span class="nt"&gt;-q&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt; &lt;span class="nt"&gt;--format&lt;/span&gt; &lt;span class="s1"&gt;'{{ .Name }}: {{ .Config.User }}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Most of them will return a blank after the colon. Blank means root. You're running production workloads as the most privileged user on the system, and nobody sent you a warning email about it.&lt;/p&gt;

&lt;p&gt;Security in Dockerfiles isn't glamorous. It doesn't make it into conference talks the way Kubernetes migrations do. But these four practices are the difference between an image you can deploy to production with confidence and one that's a liability waiting to be discovered. The &lt;a href="https://dev.to/en/tutorials/dockerfile-best-practices-efficiency"&gt;last lesson covered efficiency&lt;/a&gt; — fast builds, lean images. This one covers what keeps you employed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Don't run as root
&lt;/h2&gt;

&lt;p&gt;Every Docker container runs as &lt;code&gt;root&lt;/code&gt; by default. Not a sandboxed root, not an elevated user — actual UID 0, the same root that owns your filesystem. The container is isolated, yes, but isolation has failure modes.&lt;/p&gt;

&lt;p&gt;If your application has a vulnerability (and every application does), an attacker who compromises the process gets root-level access to everything in the container. If there's a container escape bug — rare, but they exist and get discovered — that root inside becomes root outside. The principle of least privilege isn't a DevSecOps buzzword; it's the reason you create separate database users instead of using &lt;code&gt;postgres&lt;/code&gt; for everything.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# ❌ Default — process runs as root&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; python:3.12-slim&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; requirements.txt .&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .&lt;/span&gt;
&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["python", "app.py"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# ✅ Non-root user&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; python:3.12-slim&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; requirements.txt .&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .&lt;/span&gt;

&lt;span class="c"&gt;# Create non-root user and group&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;groupadd &lt;span class="nt"&gt;--system&lt;/span&gt; appgroup &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    useradd &lt;span class="nt"&gt;--system&lt;/span&gt; &lt;span class="nt"&gt;--no-create-home&lt;/span&gt; &lt;span class="nt"&gt;--gid&lt;/span&gt; appgroup appuser

&lt;span class="c"&gt;# Transfer ownership, then switch&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nb"&gt;chown&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; appuser:appgroup /app
&lt;span class="k"&gt;USER&lt;/span&gt;&lt;span class="s"&gt; appuser&lt;/span&gt;

&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["python", "app.py"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The order matters. &lt;code&gt;WORKDIR&lt;/code&gt; creates the directory as root before your user exists. Create the user, transfer ownership, then switch — everything after &lt;code&gt;USER appuser&lt;/code&gt; runs as that user, including the final &lt;code&gt;CMD&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;You can compress the setup into one layer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;RUN &lt;/span&gt;groupadd &lt;span class="nt"&gt;--system&lt;/span&gt; appgroup &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    useradd &lt;span class="nt"&gt;--system&lt;/span&gt; &lt;span class="nt"&gt;--no-create-home&lt;/span&gt; &lt;span class="nt"&gt;--gid&lt;/span&gt; appgroup appuser &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nb"&gt;chown&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; appuser:appgroup /app
&lt;span class="k"&gt;USER&lt;/span&gt;&lt;span class="s"&gt; appuser&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Official Node.js images include a &lt;code&gt;node&lt;/code&gt; user out of the box, which saves a step:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; node:20-slim&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; package*.json .&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;npm ci &lt;span class="nt"&gt;--only&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;production
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .&lt;/span&gt;

&lt;span class="c"&gt;# node user is already there in official Node.js images&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nb"&gt;chown&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; node:node /app
&lt;span class="k"&gt;USER&lt;/span&gt;&lt;span class="s"&gt; node&lt;/span&gt;

&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["node", "server.js"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Scan for vulnerabilities
&lt;/h2&gt;

&lt;p&gt;Your image inherits every vulnerability in its base image and installed packages. A fresh &lt;code&gt;python:3.12-slim&lt;/code&gt; can carry dozens of CVEs that have nothing to do with your code. Most developers have no idea what's in their images because they've never looked.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://trivy.dev" rel="noopener noreferrer"&gt;Trivy&lt;/a&gt;&lt;/strong&gt; is the de facto standard for image scanning — open source, fast, zero configuration to get started:&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="c"&gt;# macOS and Linux (Homebrew)&lt;/span&gt;
brew &lt;span class="nb"&gt;install &lt;/span&gt;trivy

&lt;span class="c"&gt;# Ubuntu / Debian&lt;/span&gt;
wget &lt;span class="nt"&gt;-qO&lt;/span&gt; - https://aquasecurity.github.io/trivy-repo/deb/public.key | &lt;span class="se"&gt;\&lt;/span&gt;
  gpg &lt;span class="nt"&gt;--dearmor&lt;/span&gt; | &lt;span class="nb"&gt;sudo tee&lt;/span&gt; /usr/share/keyrings/trivy.gpg &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /dev/null
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"deb [signed-by=/usr/share/keyrings/trivy.gpg] &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;
  https://aquasecurity.github.io/trivy-repo/deb generic main"&lt;/span&gt; | &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nb"&gt;sudo tee&lt;/span&gt; /etc/apt/sources.list.d/trivy.list
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; trivy

&lt;span class="c"&gt;# Arch Linux (AUR)&lt;/span&gt;
paru &lt;span class="nt"&gt;-S&lt;/span&gt; trivy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Scan any image with one command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;trivy image python:3.12-slim
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;python:3.12-slim (debian 12.10)
===============================
Total: 47 (UNKNOWN: 0, LOW: 28, MEDIUM: 14, HIGH: 4, CRITICAL: 1)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's not a contrived example — base images carry OS-level CVEs from Debian, OpenSSL, and system libraries. Most are LOW or MEDIUM: background noise you live with. HIGH and CRITICAL are what you act on. Filter to see only those:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;trivy image &lt;span class="nt"&gt;--severity&lt;/span&gt; HIGH,CRITICAL my-app:latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can scan your own built images too, not just bases:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;trivy image my-app:latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you're on Docker Desktop or Docker &amp;gt;= 24, &lt;strong&gt;Docker Scout&lt;/strong&gt; is already installed and covers similar ground:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker scout cves my-app:latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Trivy tends to be more thorough and is what you'll see in most CI/CD pipelines. Scout is convenient if you prefer the Docker Desktop dashboard and don't want another CLI tool — for those who like their metrics with a GUI and don't mind clicking around (no judgment, it works).&lt;/p&gt;

&lt;h3&gt;
  
  
  Fixing vulnerabilities by rebuilding
&lt;/h3&gt;

&lt;p&gt;Scanning tells you what's there. The fix is often simpler than expected: rebuild against an updated base image.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker pull python:3.12-slim      &lt;span class="c"&gt;# Pull the latest base&lt;/span&gt;
docker build &lt;span class="nt"&gt;--no-cache&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;         &lt;span class="c"&gt;# Rebuild without cache to pick up updates&lt;/span&gt;
trivy image &lt;span class="nt"&gt;--severity&lt;/span&gt; HIGH,CRITICAL my-app:latest   &lt;span class="c"&gt;# Check again&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A surprising number of CVEs in "old" images disappear just from rebuilding. The maintainers of official images patch continuously. If you never rebuild, you never get those patches.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pin versions correctly
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://dev.to/en/tutorials/dockerfile-best-practices-efficiency"&gt;last lesson&lt;/a&gt; covered pinning your base image tag — &lt;code&gt;python:3.12.10-slim&lt;/code&gt; instead of &lt;code&gt;python:3.12-slim&lt;/code&gt; — for reproducibility. But tags have a security problem: &lt;strong&gt;tags are mutable&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Anyone with push access to a registry can overwrite a tag with different content. For official Docker Hub images this essentially never happens, but in CI/CD pipelines pulling from private registries or third-party sources, it's a real supply chain attack vector.&lt;/p&gt;

&lt;p&gt;The immutable alternative is the &lt;strong&gt;SHA256 digest&lt;/strong&gt;:&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="c"&gt;# Get the digest of an image&lt;/span&gt;
docker inspect python:3.12-slim &lt;span class="nt"&gt;--format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'{{index .RepoDigests 0}}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;python@sha256:f4f0ef4e6a9a7bbd4954d6e5f4cd89b21f483d39beee63c3a17c840c10f5dd96
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# ❌ Floating tag — can change without warning&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; python:3.12-slim&lt;/span&gt;

&lt;span class="c"&gt;# ✅ Pinned tag — better, but still mutable&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; python:3.12.10-slim-bookworm&lt;/span&gt;

&lt;span class="c"&gt;# ✅✅ SHA256 digest — immutable by definition&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; python:3.12-slim@sha256:f4f0ef4e6a9a7bbd4954d6e5f4cd89b21f483d39beee63c3a17c840c10f5dd96&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A digest is the hash of the image content. That image is exactly that image, forever, on any machine. If someone replaces the tag, the digest still points to the original content.&lt;/p&gt;

&lt;p&gt;The same logic applies to system packages. Unpinned installs grab whatever's available today:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# ❌ Unpinned — installs whatever version is current&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;apt-get update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; curl

&lt;span class="c"&gt;# ✅ Pinned version — reproducible&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;apt-get update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; &lt;span class="nv"&gt;curl&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;8.5.0-2ubuntu1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To see what versions are available:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;apt-cache madison curl
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do you need digest pinning for every project? Probably not for a personal side project. Yes for production images handling sensitive data or sitting in regulated environments.&lt;/p&gt;

&lt;h2&gt;
  
  
  Secrets: what must never be in your image
&lt;/h2&gt;

&lt;p&gt;This is where most people trip up. And it makes sense: you need credentials during development. The obvious solution is &lt;code&gt;ENV&lt;/code&gt; or &lt;code&gt;ARG&lt;/code&gt; in the Dockerfile. It's convenient, it works, and it's a ticking clock.&lt;/p&gt;

&lt;p&gt;The obvious problem is "it ends up in git." The less obvious problem is worse: &lt;strong&gt;even if it never touches git, every Docker layer is permanently recorded in the image&lt;/strong&gt;. Delete an &lt;code&gt;ENV&lt;/code&gt; in a later layer and the value is still there, visible in the build history:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# ❌ The "cleanup" layer changes nothing&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; python:3.12-slim&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; API_KEY=super-secret-key-12345&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;make some-api-call-using-the-key
&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nb"&gt;unset &lt;/span&gt;API_KEY   &lt;span class="c"&gt;# Completely useless — still in the previous layer&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker &lt;span class="nb"&gt;history &lt;/span&gt;my-image &lt;span class="nt"&gt;--no-trunc&lt;/span&gt;
&lt;span class="c"&gt;# IMAGE          CREATED BY&lt;/span&gt;
&lt;span class="c"&gt;# ...            ENV API_KEY=super-secret-key-12345  ← visible forever&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;ARG&lt;/code&gt; has the same problem, despite what the docs imply. It doesn't persist as an environment variable in the final image — but it does persist in the layer of the &lt;code&gt;RUN&lt;/code&gt; that used it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;ARG&lt;/span&gt;&lt;span class="s"&gt; API_KEY&lt;/span&gt;
&lt;span class="c"&gt;# ❌ The value is baked into this layer's metadata&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;curl &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;API_KEY&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; https://api.example.com/setup
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The right solution for build-time secrets is &lt;strong&gt;BuildKit's &lt;code&gt;--mount=type=secret&lt;/code&gt;&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# syntax=docker/dockerfile:1&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; python:3.12-slim&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .&lt;/span&gt;

&lt;span class="c"&gt;# The secret is available only during this RUN — never stored in any layer&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nt"&gt;--mount&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;secret,id&lt;span class="o"&gt;=&lt;/span&gt;api_key &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nv"&gt;API_KEY&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; /run/secrets/api_key&lt;span class="si"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    curl &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;API_KEY&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; https://api.example.com/setup
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pass the secret at build time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker build &lt;span class="nt"&gt;--secret&lt;/span&gt; &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;api_key,src&lt;span class="o"&gt;=&lt;/span&gt;./secrets/api_key.txt &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The secret is mounted in a tmpfs during that specific &lt;code&gt;RUN&lt;/code&gt; and never written to any layer. The &lt;code&gt;# syntax=docker/dockerfile:1&lt;/code&gt; line at the top enables BuildKit's extended syntax — without it, Docker won't recognize the &lt;code&gt;--mount&lt;/code&gt; option.&lt;/p&gt;

&lt;p&gt;For &lt;strong&gt;runtime secrets&lt;/strong&gt; — database passwords, API tokens your application needs while running — the answer is straightforward: &lt;strong&gt;don't put them in the image&lt;/strong&gt;. Runtime secrets are injected from outside, at deploy time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# ✅ The image contains no credentials&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; python:3.12-slim&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;groupadd &lt;span class="nt"&gt;--system&lt;/span&gt; appgroup &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    useradd &lt;span class="nt"&gt;--system&lt;/span&gt; &lt;span class="nt"&gt;--no-create-home&lt;/span&gt; &lt;span class="nt"&gt;--gid&lt;/span&gt; appgroup appuser &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nb"&gt;chown&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; appuser:appgroup /app
&lt;span class="k"&gt;USER&lt;/span&gt;&lt;span class="s"&gt; appuser&lt;/span&gt;
&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["python", "app.py"]&lt;/span&gt;
&lt;span class="c"&gt;# The app reads DATABASE_URL from the environment at runtime — never from the image&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# In development&lt;/span&gt;
docker run &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;postgres://... my-app

&lt;span class="c"&gt;# In production with Docker Compose&lt;/span&gt;
services:
  app:
    image: my-app:latest
    environment:
      DATABASE_URL: &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;  &lt;span class="c"&gt;# From the host environment, never in the Dockerfile&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For more rigorous setups: Docker Secrets in Swarm, Kubernetes Secrets with proper RBAC, or a dedicated secrets manager like HashiCorp Vault. But even the basic pattern of injecting at runtime is infinitely better than credentials baked into a layer that follows the image everywhere it goes.&lt;/p&gt;




&lt;p&gt;Non-root user, vulnerability scanning with Trivy, immutable digests instead of mutable tags, secrets out of the image. Four practices that aren't complicated — they just require actually doing them.&lt;/p&gt;

&lt;p&gt;Next up is one of the most powerful Docker features for production images: &lt;strong&gt;multi-stage builds&lt;/strong&gt;. You'll learn how to separate the build environment from the runtime environment, with real examples in Go and Node.js that shrink final image sizes from hundreds of megabytes down to a fraction.&lt;/p&gt;

&lt;p&gt;Never stop coding!&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;💡 Challenge&lt;/strong&gt;: Take your image from the previous lesson's challenge. Add a non-root user and scan it with &lt;code&gt;trivy image --severity HIGH,CRITICAL&lt;/code&gt;. Then compare against &lt;code&gt;python:3.12-alpine&lt;/code&gt; — there's a surprise waiting for you there.&lt;/p&gt;

</description>
      <category>docker</category>
      <category>beginners</category>
      <category>tutorial</category>
      <category>devops</category>
    </item>
    <item>
      <title>Git Flow: a workflow for managing releases</title>
      <dc:creator>Javi Palacios</dc:creator>
      <pubDate>Mon, 24 Aug 2026 09:17:31 +0000</pubDate>
      <link>https://dev.to/fj_palacios/git-flow-a-workflow-for-managing-releases-2a31</link>
      <guid>https://dev.to/fj_palacios/git-flow-a-workflow-for-managing-releases-2a31</guid>
      <description>&lt;p&gt;It's 4 PM and the PM just posted in the team chat: "can we ship version 2.1 today?" You open the repository to check. Feature A is half-done on its branch. Feature B hasn't been touched in three days and isn't merged. Someone is fixing a production bug locally and hasn't pushed. And &lt;code&gt;main&lt;/code&gt; has three commits from this week that nobody remembers — are they release candidates or experiments?&lt;/p&gt;

&lt;p&gt;Can you ship? What goes in and what doesn't? How do you separate what's ready from what isn't?&lt;/p&gt;

&lt;p&gt;Git Flow is the answer to exactly that question. It's not a Git feature — it's a convention about how to organize branches so the team always knows what's in production, what's in development, and how to move between them in a controlled way.&lt;/p&gt;

&lt;h2&gt;
  
  
  The core idea: two types of branches
&lt;/h2&gt;

&lt;p&gt;Before the details, the concept that holds everything together: Git Flow draws a hard line between code that &lt;strong&gt;is production&lt;/strong&gt; and code that &lt;strong&gt;is headed toward production&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Two permanent branches handle this:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;main&lt;/code&gt;&lt;/strong&gt; — always production-ready. Every commit on &lt;code&gt;main&lt;/code&gt; represents a deployable version. It never has half-finished work. Every time something lands on &lt;code&gt;main&lt;/code&gt;, it gets a version tag.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;develop&lt;/code&gt;&lt;/strong&gt; — where work in progress gets integrated. It's the team's staging branch: finished features flow into it, releases are prepared from it, and it always reflects where the next version is headed.&lt;/p&gt;

&lt;p&gt;The separation between &lt;code&gt;main&lt;/code&gt; and &lt;code&gt;develop&lt;/code&gt; is the core of Git Flow. &lt;code&gt;main&lt;/code&gt; is sacred — it only receives merges from release branches or hotfixes, never direct work.&lt;/p&gt;

&lt;h2&gt;
  
  
  The temporary branches
&lt;/h2&gt;

&lt;p&gt;On top of those two permanent branches, Git Flow defines three types of temporary branches. Each has a specific origin and a specific destination — and that's what gives the workflow its structure.&lt;/p&gt;

&lt;h3&gt;
  
  
  feature/
&lt;/h3&gt;

&lt;p&gt;Your day-to-day working branches. Following the &lt;a href="https://dev.to/en/tutorials/branch-naming-conventions-git"&gt;branch naming conventions from the last tutorial&lt;/a&gt;, each feature branch corresponds to a specific functionality or ticket.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Origin&lt;/strong&gt;: &lt;code&gt;develop&lt;/code&gt;&lt;br&gt;
&lt;strong&gt;Destination&lt;/strong&gt;: &lt;code&gt;develop&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;&lt;span class="c"&gt;# Start a feature from develop&lt;/span&gt;
git switch develop
git switch &lt;span class="nt"&gt;-c&lt;/span&gt; feature/123-user-auth

&lt;span class="c"&gt;# ... work, commit, work, commit ...&lt;/span&gt;

&lt;span class="c"&gt;# Merge back into develop&lt;/span&gt;
git switch develop
git merge &lt;span class="nt"&gt;--no-ff&lt;/span&gt; feature/123-user-auth
git branch &lt;span class="nt"&gt;-d&lt;/span&gt; feature/123-user-auth
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;--no-ff&lt;/code&gt; (no fast-forward) flag matters: it always creates an explicit merge commit even when a fast-forward is possible. That preserves in the history the fact that those commits belonged to a feature branch — useful context months later when someone's trying to understand why that code exists.&lt;/p&gt;

&lt;p&gt;Feature branches never touch &lt;code&gt;main&lt;/code&gt;. Not ever. If a feature isn't ready for the release, it simply doesn't get included.&lt;/p&gt;

&lt;h3&gt;
  
  
  release/
&lt;/h3&gt;

&lt;p&gt;When &lt;code&gt;develop&lt;/code&gt; has enough finished work to justify a new version, you create a release branch. Only bug fixes go in here — no new features. The goal is stabilization, not expansion.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Origin&lt;/strong&gt;: &lt;code&gt;develop&lt;/code&gt;&lt;br&gt;
&lt;strong&gt;Destination&lt;/strong&gt;: &lt;code&gt;main&lt;/code&gt; &lt;strong&gt;and&lt;/strong&gt; &lt;code&gt;develop&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;&lt;span class="c"&gt;# Create release branch from develop&lt;/span&gt;
git switch develop
git switch &lt;span class="nt"&gt;-c&lt;/span&gt; release/2.1.0

&lt;span class="c"&gt;# ... fix bugs, update version numbers ...&lt;/span&gt;

&lt;span class="c"&gt;# Merge into main (with version tag)&lt;/span&gt;
git switch main
git merge &lt;span class="nt"&gt;--no-ff&lt;/span&gt; release/2.1.0
git tag &lt;span class="nt"&gt;-a&lt;/span&gt; v2.1.0 &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"Release version 2.1.0"&lt;/span&gt;

&lt;span class="c"&gt;# Also merge back into develop (to keep the bugfixes)&lt;/span&gt;
git switch develop
git merge &lt;span class="nt"&gt;--no-ff&lt;/span&gt; release/2.1.0

git branch &lt;span class="nt"&gt;-d&lt;/span&gt; release/2.1.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The double merge can look surprising at first. Why also merge back into &lt;code&gt;develop&lt;/code&gt;? Because the bug fixes that went into the release branch need to live in future code too. If you only merged into &lt;code&gt;main&lt;/code&gt;, those fixes would disappear in the next version — and you'd spend a baffling amount of time re-fixing bugs that were "already fixed." This is the step teams most often forget, and the one that causes the most head-scratching six months later.&lt;/p&gt;

&lt;h3&gt;
  
  
  hotfix/
&lt;/h3&gt;

&lt;p&gt;For when something breaks in production and can't wait for the next release. It's the only branch type that starts directly from &lt;code&gt;main&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Origin&lt;/strong&gt;: &lt;code&gt;main&lt;/code&gt;&lt;br&gt;
&lt;strong&gt;Destination&lt;/strong&gt;: &lt;code&gt;main&lt;/code&gt; &lt;strong&gt;and&lt;/strong&gt; &lt;code&gt;develop&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;&lt;span class="c"&gt;# Create hotfix from main&lt;/span&gt;
git switch main
git switch &lt;span class="nt"&gt;-c&lt;/span&gt; hotfix/561-payment-gateway-timeout

&lt;span class="c"&gt;# ... fix the bug ...&lt;/span&gt;

&lt;span class="c"&gt;# Merge into main&lt;/span&gt;
git switch main
git merge &lt;span class="nt"&gt;--no-ff&lt;/span&gt; hotfix/561-payment-gateway-timeout
git tag &lt;span class="nt"&gt;-a&lt;/span&gt; v2.0.1 &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"Hotfix: payment gateway timeout"&lt;/span&gt;

&lt;span class="c"&gt;# Also merge into develop&lt;/span&gt;
git switch develop
git merge &lt;span class="nt"&gt;--no-ff&lt;/span&gt; hotfix/561-payment-gateway-timeout

git branch &lt;span class="nt"&gt;-d&lt;/span&gt; hotfix/561-payment-gateway-timeout
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same double merge, same reason: the fix needs to be in the next release too, not just in the version that's currently deployed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The full picture
&lt;/h2&gt;

&lt;p&gt;Put together, the branch diagram of a project using Git Flow looks 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;         v1.0          v2.0    v2.0.1
main  ────●─────────────●────────●────────────▶
           \           ↑ \      ↑ \
develop  ───●──●──●──●──/──●────/──●──●──▶
                 \  /       \  /
feature          ●─●         ●─●
                        ↑
                   release/2.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;develop&lt;/code&gt; advances continuously. Features branch off and merge back into it. When there's enough for a release, the release branch goes out, gets stabilized, and lands on &lt;code&gt;main&lt;/code&gt; with its version tag. Hotfixes go directly from &lt;code&gt;main&lt;/code&gt; and propagate to both branches.&lt;/p&gt;

&lt;p&gt;Does this feel like a lot of moving parts? It is. Git Flow doesn't aim to be simple — it aims to be predictable. On a team of four people, predictable is worth a lot more than simple.&lt;/p&gt;

&lt;h2&gt;
  
  
  The git-flow extension
&lt;/h2&gt;

&lt;p&gt;All of this can be done with plain Git commands (as in the examples above), but there's an extension that automates the repetitive steps: &lt;code&gt;git-flow&lt;/code&gt;, created by the same Vincent Driessen who designed the workflow in 2010 (in a blog post that has since accumulated roughly 18 million views — turns out a lot of teams had this problem).&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="c"&gt;# macOS and Linux (Homebrew)&lt;/span&gt;
brew &lt;span class="nb"&gt;install &lt;/span&gt;git-flow-avh

&lt;span class="c"&gt;# Ubuntu / Debian&lt;/span&gt;
apt &lt;span class="nb"&gt;install &lt;/span&gt;git-flow

&lt;span class="c"&gt;# Arch Linux (AUR)&lt;/span&gt;
paru &lt;span class="nt"&gt;-S&lt;/span&gt; gitflow-avh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Initialize it in your repository:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git flow init
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The command asks a few questions about branch naming (defaults are &lt;code&gt;main&lt;/code&gt;, &lt;code&gt;develop&lt;/code&gt;, &lt;code&gt;feature/&lt;/code&gt;, etc.). Once configured:&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="c"&gt;# Start a feature&lt;/span&gt;
git flow feature start 123-user-auth
&lt;span class="c"&gt;# Equivalent to: git switch -c feature/123-user-auth develop&lt;/span&gt;

&lt;span class="c"&gt;# Finish a feature&lt;/span&gt;
git flow feature finish 123-user-auth
&lt;span class="c"&gt;# Equivalent to: merge into develop, delete branch&lt;/span&gt;

&lt;span class="c"&gt;# Start a release&lt;/span&gt;
git flow release start 2.1.0

&lt;span class="c"&gt;# Finish a release&lt;/span&gt;
git flow release finish 2.1.0
&lt;span class="c"&gt;# Equivalent to: merge into main + tag + merge into develop + delete branch&lt;/span&gt;

&lt;span class="c"&gt;# Start a hotfix&lt;/span&gt;
git flow hotfix start 561-payment-timeout

&lt;span class="c"&gt;# Finish a hotfix&lt;/span&gt;
git flow hotfix finish 561-payment-timeout
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The extension doesn't do anything you can't do by hand, but it eliminates the cognitive load of remembering all the steps — especially the double merge, which is easy to skip when you're in a hurry. For those who prefer a visual interface, GitKraken and SourceTree both have native Git Flow support, if you don't mind waiting for an Electron app to load.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to use Git Flow (and when not to)
&lt;/h2&gt;

&lt;p&gt;Git Flow has a reputation that isn't entirely unfair: it's a complex workflow. Many steps, many branches, merging things in two places. Apply it to a project that doesn't justify it and you add ceremony without benefit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It makes sense when:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The project has versioned releases: libraries, SDKs, mobile apps, packaged software&lt;/li&gt;
&lt;li&gt;Multiple developers are working on parallel features that need to stay isolated from each other&lt;/li&gt;
&lt;li&gt;You need to maintain multiple production versions simultaneously (hotfixes on v2.0 while v2.1 is in development)&lt;/li&gt;
&lt;li&gt;The team has a clear, scheduled release cycle — not continuous deployment&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;It doesn't make sense when:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Your team does continuous deployment — every merge to &lt;code&gt;main&lt;/code&gt; goes to production in minutes. In that context, &lt;code&gt;develop&lt;/code&gt; adds nothing, because "production" and "in development" are practically the same thing&lt;/li&gt;
&lt;li&gt;The project is small or you're working alone&lt;/li&gt;
&lt;li&gt;Your release cadence is very short (multiple times per day)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For those cases, the next tutorial covers &lt;strong&gt;GitHub Flow&lt;/strong&gt;: a deliberately simpler workflow built for continuous deployment. One stable branch, short-lived feature branches, and deploy directly after merge. Fewer moving parts, less friction, enough structure for most modern web projects.&lt;/p&gt;




&lt;p&gt;Git Flow isn't the most popular workflow nowadays — that's probably trunk-based development. But it remains the reference for understanding how to separate work in progress from production code, and plenty of projects with versioned releases still use it for exactly that reason. Understanding it gives you the vocabulary to reason about any branching workflow, however simplified.&lt;/p&gt;




&lt;p&gt;💡 &lt;strong&gt;Challenge&lt;/strong&gt;: Think about a project you've worked on (or know well). Which scenario fits it better: versioned releases with parallel work, or continuous deployment? Would Git Flow add value or just overhead? Reason through the answer using the criteria from this lesson.&lt;/p&gt;

&lt;p&gt;Never stop coding!&lt;/p&gt;

</description>
      <category>git</category>
      <category>beginners</category>
      <category>tutorial</category>
      <category>devops</category>
    </item>
    <item>
      <title>Branch naming conventions in Git</title>
      <dc:creator>Javi Palacios</dc:creator>
      <pubDate>Sat, 22 Aug 2026 12:31:44 +0000</pubDate>
      <link>https://dev.to/fj_palacios/branch-naming-conventions-in-git-4jid</link>
      <guid>https://dev.to/fj_palacios/branch-naming-conventions-in-git-4jid</guid>
      <description>&lt;p&gt;You join a new project. Clone the repo. Run &lt;code&gt;git branch -a&lt;/code&gt; to get a feel for the landscape. This is what you find:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  remotes/origin/test
  remotes/origin/test2
  remotes/origin/test-final
  remotes/origin/johns-branch
  remotes/origin/new-feature
  remotes/origin/new-feature-v2
  remotes/origin/TEMP
  remotes/origin/fix
  remotes/origin/actual-fix
  remotes/origin/DO-NOT-MERGE
  remotes/origin/main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Eleven branches. Zero information. Which ones are merged? What's in &lt;code&gt;johns-branch&lt;/code&gt; — and does John still work here? When was &lt;code&gt;TEMP&lt;/code&gt; created, and does anyone know what happens if you delete it? What's the difference between &lt;code&gt;fix&lt;/code&gt; and &lt;code&gt;actual-fix&lt;/code&gt;, and which one made it to production?&lt;/p&gt;

&lt;p&gt;The problem isn't that there are branches. Branches are one of Git's best features. The problem is that without a naming convention, each developer on the team invents their own system — and the remote ends up looking like a graveyard where you know things are buried but have no idea where.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why branch names matter
&lt;/h2&gt;

&lt;p&gt;It's easy to think of a branch name as a minor detail, a label that only matters while the branch is alive. That framing is wrong.&lt;/p&gt;

&lt;p&gt;A branch name is a temporary contract with your team. It communicates what kind of work the branch contains, which ticket or feature it's tied to, and when it's appropriate to delete it. Without that contract, every branch interaction starts with the same question: "wait, what is this?"&lt;/p&gt;

&lt;p&gt;Three signs you have a naming problem:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Someone asks "can I delete this branch?" and no one knows the answer&lt;/li&gt;
&lt;li&gt;You run &lt;code&gt;git branch -a&lt;/code&gt; and spend more time reading names than doing actual work&lt;/li&gt;
&lt;li&gt;There are two active branches called &lt;code&gt;feature-new&lt;/code&gt; and &lt;code&gt;feature-new-v2&lt;/code&gt; and no one remembers which one is current&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A good naming convention makes the branch self-documenting.&lt;/p&gt;

&lt;h2&gt;
  
  
  The common patterns
&lt;/h2&gt;

&lt;p&gt;Most teams use a prefix that describes the type of work, separated from the rest of the name by a forward slash:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;feature/user-authentication
fix/null-check-in-api-endpoint
hotfix/critical-payment-failure
release/2.3.0
chore/update-dependencies
docs/api-reference-v2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The most common prefixes:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Prefix&lt;/th&gt;
&lt;th&gt;When to use&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;feature/&lt;/code&gt; or &lt;code&gt;feat/&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;New functionality&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;fix/&lt;/code&gt; or &lt;code&gt;bugfix/&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Bug fix during active development&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hotfix/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Urgent fix going straight to production&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;release/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Release preparation branch&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;chore/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Maintenance: dependencies, config&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;docs/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Documentation-only changes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;refactor/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Code refactoring without behavior change&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;test/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Adding or fixing tests&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If the &lt;a href="https://dev.to/en/tutorials/git-commit-best-practices"&gt;commit best practices&lt;/a&gt; tutorial from last lesson looks familiar, you'll notice the prefixes map directly to Conventional Commits types — that's not a coincidence. When your branches and your commits speak the same language, the project history becomes much easier to read at a glance.&lt;/p&gt;

&lt;p&gt;You don't need to use all of them. A small team running &lt;code&gt;feature/&lt;/code&gt;, &lt;code&gt;fix/&lt;/code&gt;, and &lt;code&gt;hotfix/&lt;/code&gt; has everything it needs. The key isn't exhaustiveness — it's consistency.&lt;/p&gt;

&lt;h2&gt;
  
  
  Including the issue number
&lt;/h2&gt;

&lt;p&gt;The type prefix is half the information. The other half is knowing which ticket or issue the branch corresponds to. The most widely used convention puts the issue number before the description:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;feature/123-user-authentication
fix/482-null-response-user-endpoint
hotfix/561-payment-gateway-timeout
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The number comes first for a practical reason: branches sort alphabetically in most tools, and putting the number first automatically groups branches for the same issue together. It also makes searching trivial — if you know the ticket number, you can filter directly.&lt;/p&gt;

&lt;p&gt;GitHub and GitLab also recognize issue numbers in branch names and link them automatically in the interface — a small detail that saves a surprising amount of navigation over time.&lt;/p&gt;

&lt;p&gt;If your team doesn't use an issue tracker (it happens, no judgment), just use the description alone. But if you do, the number is free and there's no reason not to include it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Format rules
&lt;/h2&gt;

&lt;p&gt;A branch name is going to appear in &lt;code&gt;git log&lt;/code&gt;, in pull requests, in CI/CD output, and possibly in changelogs. Worth making it look right in all of those contexts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Always kebab-case&lt;/strong&gt;: lowercase, words separated by hyphens. No spaces (Git converts them and the result is a mess), no underscores, no camelCase.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;feature/user-auth          &lt;span class="c"&gt;# ✅&lt;/span&gt;
feature/userAuth           &lt;span class="c"&gt;# ❌&lt;/span&gt;
feature/user_auth          &lt;span class="c"&gt;# ❌&lt;/span&gt;
feature/User Auth          &lt;span class="c"&gt;# ❌ (and Git will complain)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Descriptive but not a paragraph&lt;/strong&gt;: the name should be readable in &lt;code&gt;git log --oneline&lt;/code&gt;. &lt;code&gt;feature/123-add-email-verification-to-user-registration-flow&lt;/code&gt; is technically correct but unnecessarily verbose. &lt;code&gt;feature/123-email-verification&lt;/code&gt; says the same thing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Avoid "new", "final", "v2"&lt;/strong&gt;: these names age badly. &lt;code&gt;feature/new-login&lt;/code&gt; becomes meaningless as soon as there's a second login implementation. &lt;code&gt;hotfix/final-fix&lt;/code&gt; is a promise no software can keep.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No personal names on shared branches&lt;/strong&gt;: &lt;code&gt;johns-branch&lt;/code&gt; is fine locally while you're exploring something. On the remote, a personal name carries no information relevant to the rest of the team — and once John leaves, the branch sits there forever, a mystery for everyone who comes after.&lt;/p&gt;

&lt;h2&gt;
  
  
  Team conventions vs personal preferences
&lt;/h2&gt;

&lt;p&gt;If you work alone, you can be as informal as you like with local branch names. No one's going to judge a branch called &lt;code&gt;wip-attempt-3&lt;/code&gt; on your machine.&lt;/p&gt;

&lt;p&gt;On the remote, though, names are public. And a name that makes perfect sense to you right now might mean nothing to a teammate next week, or to yourself in three months (which is essentially the same thing).&lt;/p&gt;

&lt;p&gt;The most sustainable way to keep a convention alive on a team is to document it somewhere visible before someone creates their first branch — in the &lt;code&gt;README.md&lt;/code&gt;, in &lt;code&gt;CONTRIBUTING.md&lt;/code&gt;, or better yet, enforce it with a Git hook that validates the format automatically. It doesn't need to be sophisticated — a &lt;code&gt;pre-push&lt;/code&gt; script that checks the name starts with a recognized prefix already eliminates 90% of poorly named branches without anyone having to play enforcer.&lt;/p&gt;

&lt;p&gt;If you're like me when I started, conventions can feel like unnecessary overhead. The project is moving fast, the branch name is the least of your problems, you'll sort it out later. You won't. And when you have 80 branches on the remote and no one knows what's safe to delete, you'll remember this conversation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Branch lifecycle
&lt;/h2&gt;

&lt;p&gt;Branches have an expiration date. They're created for a specific purpose, they get merged, and they should die. In practice, they rarely do.&lt;/p&gt;

&lt;p&gt;To see which branches are already merged into &lt;code&gt;main&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;git branch &lt;span class="nt"&gt;--merged&lt;/span&gt; main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To delete a local branch that's been merged (safe — Git won't delete it if it has unmerged changes):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git branch &lt;span class="nt"&gt;-d&lt;/span&gt; feature/123-user-auth
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To delete it even if it hasn't been merged (you're scrapping that work):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git branch &lt;span class="nt"&gt;-D&lt;/span&gt; feature/abandoned-experiment
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The capital &lt;code&gt;D&lt;/code&gt; is intentional — it's the "do it even though you think it's a bad idea" flag. Use it when you know what you're doing, not as a way to make Git stop complaining.&lt;/p&gt;

&lt;p&gt;To delete a branch on the remote:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git push origin &lt;span class="nt"&gt;--delete&lt;/span&gt; feature/123-user-auth
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And to clean up local references to remote branches that no longer exist:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git fetch &lt;span class="nt"&gt;--prune&lt;/span&gt;
&lt;span class="c"&gt;# or equivalently&lt;/span&gt;
git remote prune origin
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you'd rather manage all of this visually, &lt;code&gt;lazygit&lt;/code&gt; has a dedicated branch view where you can browse, delete, and navigate entirely by keyboard — no syntax to remember, no Electron, no fan heroics. For those who prefer a mouse, GitLens in VS Code covers this reasonably well too.&lt;/p&gt;




&lt;p&gt;A naming convention doesn't fix architectural problems or make the code better. But it eliminates an entire class of questions that shouldn't be questions: what's in here? Is this merged? Can I delete it? With consistent names, those answers live in the name itself.&lt;/p&gt;

&lt;p&gt;In the next tutorial, we'll look at &lt;strong&gt;Git Flow&lt;/strong&gt;: the workflow that takes these branch prefixes and formalizes them into a structured process with clear roles for &lt;code&gt;main&lt;/code&gt;, &lt;code&gt;develop&lt;/code&gt;, &lt;code&gt;feature/&lt;/code&gt;, &lt;code&gt;release/&lt;/code&gt;, and &lt;code&gt;hotfix/&lt;/code&gt; — a way to coordinate work across a team where multiple people are pushing to the same repository at the same time.&lt;/p&gt;




&lt;p&gt;💡 &lt;strong&gt;Challenge&lt;/strong&gt;: Open a repository you've worked on (or any popular open source project on GitHub) and run &lt;code&gt;git branch -a&lt;/code&gt;. Evaluate the names against the conventions in this lesson — which ones are self-documenting? Which ones require additional context to understand? If you were naming those branches today, what would you call them?&lt;/p&gt;

&lt;p&gt;Never stop coding!&lt;/p&gt;

</description>
      <category>git</category>
      <category>beginners</category>
      <category>tutorial</category>
      <category>devops</category>
    </item>
    <item>
      <title>Visual mode in Vim: select, operate, own it</title>
      <dc:creator>Javi Palacios</dc:creator>
      <pubDate>Fri, 21 Aug 2026 11:35:18 +0000</pubDate>
      <link>https://dev.to/fj_palacios/visual-mode-in-vim-select-operate-own-it-247n</link>
      <guid>https://dev.to/fj_palacios/visual-mode-in-vim-select-operate-own-it-247n</guid>
      <description>&lt;p&gt;Most people discover Visual mode and feel a small wave of relief: finally, something familiar. Select text, then do something with it. That's how every other editor works. That's how your hands already think.&lt;/p&gt;

&lt;p&gt;That's a perfectly fine starting point. The problem is if you stop there — using Visual mode as "keyboard-driven mouse selection" and nothing else. Because there are two things here that go well beyond that: operators that behave differently in this context, and a block mode that does things VS Code needs a plugin to simulate.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three Visual modes
&lt;/h2&gt;

&lt;p&gt;Visual mode isn't one thing — it's three, each with distinct behavior:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Command&lt;/th&gt;
&lt;th&gt;Mode&lt;/th&gt;
&lt;th&gt;What it selects&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;v&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Characterwise&lt;/td&gt;
&lt;td&gt;Character by character&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;V&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Linewise&lt;/td&gt;
&lt;td&gt;Full lines, regardless of cursor position&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Ctrl+v&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Blockwise&lt;/td&gt;
&lt;td&gt;A rectangular region (columns across multiple lines)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;v&lt;/code&gt; is the intuitive one. Press &lt;code&gt;v&lt;/code&gt;, move the cursor, the selection grows. Every Normal mode motion works: &lt;code&gt;w&lt;/code&gt;, &lt;code&gt;e&lt;/code&gt;, &lt;code&gt;$&lt;/code&gt;, &lt;code&gt;gg&lt;/code&gt;, &lt;code&gt;G&lt;/code&gt;, even search patterns. If you can navigate in Normal mode, you already know how to select in characterwise Visual.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;V&lt;/code&gt; always grabs full lines. It doesn't matter where the cursor is when you enter — you get the entire current line, and each movement extends by whole lines. Useful when you're working with code blocks where character-level precision doesn't add anything.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Ctrl+v&lt;/code&gt; is the interesting one. Its own section.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;gv&lt;/code&gt;: bring back the last selection
&lt;/h3&gt;

&lt;p&gt;A small command worth knowing early: after operating on a selection and returning to Normal mode, &lt;code&gt;gv&lt;/code&gt; restores exactly your last visual selection. Useful when you indented with &lt;code&gt;&amp;gt;&lt;/code&gt; but not enough, or when you want a second operation on the same range without reselecting from scratch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Operating on a selection
&lt;/h2&gt;

&lt;p&gt;With text selected, the most useful commands are:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Command&lt;/th&gt;
&lt;th&gt;What it does to the selection&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;d&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Delete the selection&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;c&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Delete the selection and enter Insert&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;y&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Yank (copy) the selection&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Indent the selection&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Dedent the selection&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;=&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Auto-format the selection&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;~&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Toggle case&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;u&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Lowercase&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;U&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Uppercase&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Don't try to memorize all of this now. &lt;code&gt;d&lt;/code&gt;, &lt;code&gt;y&lt;/code&gt;, and &lt;code&gt;&amp;gt;&lt;/code&gt; cover 90% of real-world usage. The rest will show up naturally when you need them.&lt;/p&gt;

&lt;p&gt;One combination worth learning early: &lt;code&gt;vap&lt;/code&gt; — enters Visual mode and selects the entire paragraph (the text object &lt;code&gt;ap&lt;/code&gt; = "a paragraph"). In code, a paragraph is a contiguous block with no blank lines separating it: a Python function, an HTML element, a JSON object. &lt;code&gt;vap&lt;/code&gt; + &lt;code&gt;&amp;gt;&lt;/code&gt; indents it, &lt;code&gt;vap&lt;/code&gt; + &lt;code&gt;d&lt;/code&gt; deletes it, &lt;code&gt;vap&lt;/code&gt; + &lt;code&gt;y&lt;/code&gt; copies it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Block Visual mode
&lt;/h2&gt;

&lt;p&gt;Here's where things get genuinely interesting.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Ctrl+v&lt;/code&gt; activates blockwise Visual mode. Instead of selecting characters or full lines, you select a &lt;strong&gt;rectangle&lt;/strong&gt;: a column range across multiple lines simultaneously. Move three lines down and five columns right — you've selected exactly that rectangular area.&lt;/p&gt;

&lt;p&gt;The most common use case: inserting the same text at the start of multiple lines at once.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight viml"&gt;&lt;code&gt;&lt;span class="c"&gt;" You have this code:&lt;/span&gt;
&lt;span class="k"&gt;const&lt;/span&gt; name &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Alice'&lt;/span&gt;
&lt;span class="k"&gt;const&lt;/span&gt; age  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;30&lt;/span&gt;
&lt;span class="k"&gt;const&lt;/span&gt; role &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'admin'&lt;/span&gt;

&lt;span class="c"&gt;" You want to comment out all three lines with //&lt;/span&gt;
&lt;span class="c"&gt;" 1. Place the cursor on the 'c' of the first line&lt;/span&gt;
&lt;span class="c"&gt;" 2. Ctrl+v → activate blockwise Visual&lt;/span&gt;
&lt;span class="c"&gt;" 3. 2j → extend the selection two lines down&lt;/span&gt;
&lt;span class="c"&gt;" 4. I → enter Insert at the start of the selection&lt;/span&gt;
&lt;span class="c"&gt;" 5. //  → type the comment (you only see it on the first line)&lt;/span&gt;
&lt;span class="c"&gt;" 6. Esc → Vim replays the Insert across every line in the block&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;Esc&lt;/code&gt; at the end is the magic moment: what you typed appears on all the lines at once. This is Vim's multiple cursors — no plugins, no &lt;code&gt;Ctrl+D&lt;/code&gt; from VS Code, no &lt;code&gt;Alt+click&lt;/code&gt;. Just &lt;code&gt;Ctrl+v&lt;/code&gt;, &lt;code&gt;I&lt;/code&gt;, text, &lt;code&gt;Esc&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The same mechanic works with &lt;code&gt;A&lt;/code&gt; instead of &lt;code&gt;I&lt;/code&gt; to append at the end of each line — even if the lines have different lengths, Vim extends the selection to match the longest one.&lt;/p&gt;

&lt;p&gt;If this feels like a magic trick, that's because it kind of is. Block Visual mode is one of those features that turns someone who uses Vim because they have to into someone who starts to get why people are so loud about it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Visual vs Vim's grammar: when to use each
&lt;/h2&gt;

&lt;p&gt;After learning Visual mode, someone always asks: "why bother selecting with Visual when I can just do &lt;code&gt;d3w&lt;/code&gt; or &lt;code&gt;ci"&lt;/code&gt; straight from Normal mode?"&lt;/p&gt;

&lt;p&gt;Fair question. Honest answer: each one wins in different situations.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use Visual when:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You don't know exactly where the range ends and need to see it before committing&lt;/li&gt;
&lt;li&gt;The selection has an irregular shape that no single motion describes cleanly&lt;/li&gt;
&lt;li&gt;You want to apply multiple operations to the same range (&lt;code&gt;gv&lt;/code&gt; + operation)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Use operator-motion when:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You know exactly what you want and the motion describes it precisely&lt;/li&gt;
&lt;li&gt;You're going to repeat the operation with &lt;code&gt;.&lt;/code&gt; — because Visual mode breaks that repeatability&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last point is what most people miss early on. When you do &lt;code&gt;d3w&lt;/code&gt; in Normal mode, &lt;code&gt;.&lt;/code&gt; can replay it exactly — three words deleted, wherever the cursor ends up next. When you select with &lt;code&gt;v&lt;/code&gt; and then delete, &lt;code&gt;.&lt;/code&gt; only repeats the deletion but not the selection. It loses the "shape" of the change.&lt;/p&gt;

&lt;p&gt;If you're making the same edit in multiple places, &lt;a href="https://dev.to/en/tutorials/normal-mode-power-of-vim"&gt;Normal mode's grammar&lt;/a&gt; with operators and motions is faster and repeatable. If you're doing it once and need to see the range before confirming, Visual is the right call.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key concepts from this lesson
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Three variants&lt;/strong&gt;: &lt;code&gt;v&lt;/code&gt; (characterwise), &lt;code&gt;V&lt;/code&gt; (linewise), &lt;code&gt;Ctrl+v&lt;/code&gt; (blockwise)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;gv&lt;/code&gt;&lt;/strong&gt; restores the last visual selection — operate on the same range without reselecting&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;vap&lt;/code&gt;&lt;/strong&gt; selects the entire paragraph — works well with code blocks&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Block Visual&lt;/strong&gt; + &lt;code&gt;I&lt;/code&gt; / &lt;code&gt;A&lt;/code&gt; + text + &lt;code&gt;Esc&lt;/code&gt; = multiple cursors with no plugins&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;When to use Visual&lt;/strong&gt;: uncertain range, irregular shape, no need to repeat with &lt;code&gt;.&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;When to use operator-motion&lt;/strong&gt;: known range, repeatable change, serial edits&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Visual mode completes the trio of core Vim modes. From here the learning curve shifts: what comes next isn't more modes — it's ways to move faster. The next tutorial covers &lt;strong&gt;searching with &lt;code&gt;/&lt;/code&gt;, &lt;code&gt;?&lt;/code&gt;, and &lt;code&gt;*&lt;/code&gt;&lt;/strong&gt; — how to jump anywhere in a file in a handful of keystrokes.&lt;/p&gt;

&lt;p&gt;Never stop coding!&lt;/p&gt;

</description>
      <category>vim</category>
      <category>beginners</category>
      <category>tutorial</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Lists in Python: your first data structure</title>
      <dc:creator>Javi Palacios</dc:creator>
      <pubDate>Thu, 20 Aug 2026 10:49:47 +0000</pubDate>
      <link>https://dev.to/fj_palacios/lists-in-python-your-first-data-structure-4bha</link>
      <guid>https://dev.to/fj_palacios/lists-in-python-your-first-data-structure-4bha</guid>
      <description>&lt;p&gt;At some point, one variable isn't enough. You're writing a program to track a shopping cart, a list of players in a game, or the results of a search — and you need to store multiple values that belong together. You could create &lt;code&gt;product_1&lt;/code&gt;, &lt;code&gt;product_2&lt;/code&gt;, &lt;code&gt;product_3&lt;/code&gt;... but we both know where that ends up.&lt;/p&gt;

&lt;p&gt;That's what lists are for.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is a list?
&lt;/h2&gt;

&lt;p&gt;A &lt;strong&gt;list&lt;/strong&gt; is an &lt;strong&gt;ordered&lt;/strong&gt; collection of elements. Ordered doesn't mean alphabetically sorted — it means every element has a fixed &lt;strong&gt;position&lt;/strong&gt;, and that position is how you reach it. Think of it as a numbered row of slots: you know exactly where everything is.&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="n"&gt;products&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;shirt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pants&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sneakers&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;numbers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;mixed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hello&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;3.14&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;   &lt;span class="c1"&gt;# Python allows mixed types
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Square brackets, elements separated by commas. If you're coming from Java or C, you're waiting for the type declaration and the size. Python doesn't ask. A list is created with whatever you give it, grows when you need it to grow, and shrinks when you remove things.&lt;/p&gt;

&lt;p&gt;An empty list is perfectly fine — you'll create it first and fill it as you go:&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="n"&gt;cart&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;   &lt;span class="c1"&gt;# Empty list, will be filled as the user shops
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Accessing elements: the index
&lt;/h2&gt;

&lt;p&gt;Every position in a list has a number called an &lt;strong&gt;index&lt;/strong&gt;. Indices start at &lt;code&gt;0&lt;/code&gt;, not &lt;code&gt;1&lt;/code&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="n"&gt;fruits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;apple&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pear&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;grape&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;kiwi&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="c1"&gt;#           0         1       2        3
&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fruits&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;   &lt;span class="c1"&gt;# "apple"
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fruits&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;   &lt;span class="c1"&gt;# "grape"
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first element is always &lt;code&gt;0&lt;/code&gt;. It seems arbitrary until you start doing position arithmetic and it makes complete sense — but that's a conversation for another day.&lt;/p&gt;

&lt;p&gt;Try to access an index that doesn't exist, and Python won't guess or make something up:&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="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fruits&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;   &lt;span class="c1"&gt;# IndexError: list index out of range
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;IndexError&lt;/code&gt; is the most common error when working with lists. You'll see it. You'll be annoyed by it exactly once. After that, you'll know where the problem is before you even finish reading the traceback.&lt;/p&gt;

&lt;h3&gt;
  
  
  Negative indices
&lt;/h3&gt;

&lt;p&gt;What if you need the last element of a list but you don't know how many elements it has? Calculate the length and subtract one? Python has something better: &lt;strong&gt;negative indices&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;[-1]&lt;/code&gt; is always the last element. &lt;code&gt;[-2]&lt;/code&gt; is second to last. And so on:&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="n"&gt;fruits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;apple&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pear&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;grape&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;kiwi&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fruits&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;   &lt;span class="c1"&gt;# "kiwi"  — last
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fruits&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;   &lt;span class="c1"&gt;# "grape" — second to last
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fruits&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;   &lt;span class="c1"&gt;# "apple" — same as [0]
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without negative indices, "give me the last element" would be &lt;code&gt;fruits[len(fruits) - 1]&lt;/code&gt;. With them, it's &lt;code&gt;fruits[-1]&lt;/code&gt;. The difference isn't just shorter — it's that when you read this code three weeks from now, &lt;code&gt;fruits[-1]&lt;/code&gt; says exactly what it does with no mental overhead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Slicing: extracting a range of elements
&lt;/h2&gt;

&lt;p&gt;You've been accessing one element at a time with &lt;code&gt;[index]&lt;/code&gt;. &lt;strong&gt;Slicing&lt;/strong&gt; lets you grab a range of elements with &lt;code&gt;[start:end]&lt;/code&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="n"&gt;fruits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;apple&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pear&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;grape&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;kiwi&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;orange&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fruits&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;   &lt;span class="c1"&gt;# ["pear", "grape"] — indices 1 and 2, not 3
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fruits&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;    &lt;span class="c1"&gt;# ["apple", "pear"] — from start to index 1
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fruits&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:])&lt;/span&gt;    &lt;span class="c1"&gt;# ["grape", "kiwi", "orange"] — from index 2 to end
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fruits&lt;/span&gt;&lt;span class="p"&gt;[:])&lt;/span&gt;     &lt;span class="c1"&gt;# All elements — a full copy
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The pattern is the same as &lt;code&gt;range()&lt;/code&gt;: start is included, end is not. If you remember that from &lt;a href="https://dev.to/en/tutorials/for-loops-range-python"&gt;the previous lesson on for loops&lt;/a&gt;, you already know how this works — Python uses this convention consistently.&lt;/p&gt;

&lt;p&gt;You can also add a step, just like &lt;code&gt;range(start, stop, step)&lt;/code&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="n"&gt;numbers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;numbers&lt;/span&gt;&lt;span class="p"&gt;[::&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;    &lt;span class="c1"&gt;# [0, 2, 4, 6, 8] — every other element
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;numbers&lt;/span&gt;&lt;span class="p"&gt;[::&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;   &lt;span class="c1"&gt;# [9, 8, 7, 6, 5, 4, 3, 2, 1, 0] — reversed
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;[::-1]&lt;/code&gt; to reverse a list is one of those Stack Overflow answers that gets forty upvotes and zero comments explaining what's happening. It's not magic — it's a negative step that walks the list from the end to the beginning. Now you know what you're looking at when you see it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Iterating with for
&lt;/h2&gt;

&lt;p&gt;You already did this in the previous lesson. &lt;code&gt;for&lt;/code&gt; loops and lists are a natural fit — lists are exactly the kind of sequence the Python &lt;code&gt;for&lt;/code&gt; was built for:&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="n"&gt;fruits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;apple&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pear&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;grape&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;kiwi&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;fruit&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;fruits&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fruit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;apple
pear
grape
kiwi
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you need the index alongside the value, use &lt;code&gt;enumerate()&lt;/code&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="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fruit&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fruits&lt;/span&gt;&lt;span class="p"&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;index&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;fruit&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;0: apple
1: pear
2: grape
3: kiwi
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;enumerate()&lt;/code&gt; gives you pairs of (index, element). It's the right way to iterate over a list when you need the position. The alternative — looping with &lt;code&gt;range(len(fruits))&lt;/code&gt; and accessing with &lt;code&gt;fruits[i]&lt;/code&gt; — works, but it's a tell that you learned another language before Python.&lt;/p&gt;

&lt;h2&gt;
  
  
  len() and the in operator
&lt;/h2&gt;

&lt;p&gt;Two tools you'll use constantly with lists:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;len()&lt;/code&gt;&lt;/strong&gt; returns the number of elements:&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="n"&gt;fruits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;apple&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pear&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;grape&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;kiwi&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fruits&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;   &lt;span class="c1"&gt;# 4
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;code&gt;in&lt;/code&gt;&lt;/strong&gt; checks if an element is in the list:&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="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pear&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;fruits&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;     &lt;span class="c1"&gt;# True
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;banana&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;fruits&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# False
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And &lt;code&gt;not in&lt;/code&gt; for the opposite:&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="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;banana&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;fruits&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;No bananas here&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Together, these answer the two most common questions you'll have about a collection: how many things are in here, and is this specific thing in here? With &lt;code&gt;len()&lt;/code&gt; and &lt;code&gt;in&lt;/code&gt;, 80% of those checks are a single line.&lt;/p&gt;




&lt;p&gt;You can now create lists, access elements by position (positive and negative), extract ranges with slicing, iterate with &lt;code&gt;for&lt;/code&gt; and &lt;code&gt;enumerate()&lt;/code&gt;, and check membership with &lt;code&gt;in&lt;/code&gt;. That covers the &lt;strong&gt;reading&lt;/strong&gt; side. The next tutorial covers &lt;strong&gt;writing&lt;/strong&gt;: adding elements with &lt;code&gt;append()&lt;/code&gt;, removing them with &lt;code&gt;remove()&lt;/code&gt;, and a first look at list comprehensions — one of those Python features that feels like it shouldn't work until you run it and it just does.&lt;/p&gt;

&lt;p&gt;Never stop coding!&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;💡 Challenge&lt;/strong&gt;: Create a list with the names of five movies you like. Then: (1) print the first and last using positive and negative indices respectively; (2) display the three in the middle using slicing; (3) ask the user for a movie title and tell them whether it's in your list; (4) print all movies numbered from 1 onward using &lt;code&gt;enumerate()&lt;/code&gt;. Bonus: add a sixth movie before printing the full list (you'll need &lt;code&gt;append()&lt;/code&gt; — covered in the next tutorial, but feel free to look it up).&lt;/p&gt;

</description>
      <category>python</category>
      <category>beginners</category>
      <category>tutorial</category>
      <category>programming</category>
    </item>
    <item>
      <title>Dockerfile Best Practices: Layer Caching and Efficiency</title>
      <dc:creator>Javi Palacios</dc:creator>
      <pubDate>Wed, 19 Aug 2026 14:04:17 +0000</pubDate>
      <link>https://dev.to/fj_palacios/dockerfile-best-practices-layer-caching-and-efficiency-iep</link>
      <guid>https://dev.to/fj_palacios/dockerfile-best-practices-layer-caching-and-efficiency-iep</guid>
      <description>&lt;p&gt;You change one line in your app. Save. Run &lt;code&gt;docker build&lt;/code&gt;. And wait. And wait.&lt;/p&gt;

&lt;p&gt;Two and a half minutes for a one-line change. Exactly as long as it takes when it installs all your dependencies from scratch. Because it does — every single time.&lt;/p&gt;

&lt;p&gt;Here's where it gets interesting.&lt;/p&gt;

&lt;p&gt;Docker has a layer caching system that, when used correctly, makes 90% of your builds nearly instant. The catch is that it doesn't behave in an obvious way until someone explains it. Every Dockerfile instruction creates a layer, and Docker only rebuilds starting from the first layer that changed. Everything before it? Straight from cache.&lt;/p&gt;

&lt;p&gt;The order of your instructions isn't decorative. It's performance.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Docker's layer cache works
&lt;/h2&gt;

&lt;p&gt;When you run &lt;code&gt;docker build&lt;/code&gt;, Docker processes each instruction in order and checks: did I already build this layer? If the instruction and its inputs are identical to last time, it uses the cache. If anything changed, it rebuilds that layer from scratch — and every layer after it too.&lt;/p&gt;

&lt;p&gt;That "every layer after it" is what bites you. The cache invalidates in a cascade. Once one layer changes, all subsequent layers get rebuilt even if nothing about them changed.&lt;/p&gt;

&lt;p&gt;Look at this classic Dockerfile from someone who just learned the basics:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# ❌ Layer order that destroys the cache&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; python:3.12-slim&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .                                    # Copy everything first&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt         &lt;span class="c"&gt;# Then install dependencies&lt;/span&gt;
&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["python", "app.py"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The problem? &lt;code&gt;COPY . .&lt;/code&gt; copies your entire project — app code, config files, assets, everything. Every time you change a single line of code, that layer gets invalidated. And everything after it — including dependency installation — rebuilds from scratch.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# ✅ Layer order that uses the cache&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; python:3.12-slim&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; requirements.txt .                     # Only the dependency manifest&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt         &lt;span class="c"&gt;# Cached unless requirements.txt changed&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .                                    # Code goes last&lt;/span&gt;
&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["python", "app.py"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With this order, Docker only reinstalls dependencies when &lt;code&gt;requirements.txt&lt;/code&gt; actually changes. Modify your app code? The first three layers hit cache and the build takes seconds.&lt;/p&gt;

&lt;p&gt;The golden rule: &lt;strong&gt;what changes rarely goes near the top; what changes often goes near the bottom&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this looks like in practice
&lt;/h2&gt;

&lt;p&gt;This isn't premature optimization. On a medium-sized Python project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# Badly ordered Dockerfile — you change app.py:&lt;/span&gt;
[+] Building 47.3s (8/8) FINISHED
 =&amp;gt; [1/4] FROM python:3.12-slim                0.1s  (cached)
 =&amp;gt; [2/4] WORKDIR /app                         0.0s  (cached)
 =&amp;gt; [3/4] COPY . .                             0.4s  ← full project copy
 =&amp;gt; [4/4] RUN pip install -r requirements.txt 46.1s  ← reinstalls everything
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# Well-ordered Dockerfile — you change app.py:&lt;/span&gt;
[+] Building 0.8s (8/8) FINISHED
 =&amp;gt; [1/4] FROM python:3.12-slim                0.0s  (cached)
 =&amp;gt; [2/4] WORKDIR /app                         0.0s  (cached)
 =&amp;gt; [3/4] COPY requirements.txt .              0.0s  (cached)
 =&amp;gt; [4/4] RUN pip install -r requirements.txt  0.0s  (cached) ← already done
 =&amp;gt; [5/5] COPY . .                             0.2s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;47 seconds versus under one. Same Dockerfile, different order.&lt;/p&gt;

&lt;p&gt;If you're like me when I started, you've been through that loop: make a tiny change, kick off the build, go refill your water, come back just in time to see it finish. The right layer order eliminates that loop entirely.&lt;/p&gt;

&lt;h2&gt;
  
  
  Combining &lt;code&gt;RUN&lt;/code&gt; commands
&lt;/h2&gt;

&lt;p&gt;Every &lt;code&gt;RUN&lt;/code&gt; instruction in your Dockerfile creates a new layer. Each layer adds overhead to image size and pull times. The trick is combining related &lt;code&gt;RUN&lt;/code&gt; instructions into one, especially when installing system packages:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# ❌ Three unnecessary layers&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;apt-get update
&lt;span class="k"&gt;RUN &lt;/span&gt;apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; curl wget
&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; /var/lib/apt/lists/&lt;span class="k"&gt;*&lt;/span&gt;

&lt;span class="c"&gt;# ✅ One single layer&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;apt-get update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    curl &lt;span class="se"&gt;\
&lt;/span&gt;    wget &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; /var/lib/apt/lists/&lt;span class="k"&gt;*&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;rm -rf /var/lib/apt/lists/*&lt;/code&gt; cleanup at the end is critical — and it &lt;strong&gt;must be in the same &lt;code&gt;RUN&lt;/code&gt;&lt;/strong&gt;. Put it in a separate &lt;code&gt;RUN&lt;/code&gt; and Docker creates a layer that "deletes" those files, but they still exist in the previous layer. Your image ends up the same size with an extra layer that pretends to clean up without actually doing it.&lt;/p&gt;

&lt;p&gt;For Node.js, the equivalent:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# ✅ Install and clean in a single step&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;npm ci &lt;span class="nt"&gt;--only&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;production &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm cache clean &lt;span class="nt"&gt;--force&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Don't stress about cramming everything into one giant monolithic &lt;code&gt;RUN&lt;/code&gt;. Combine instructions that are logically related — install plus clean, configure plus compile. Instructions that don't share state can stay separate.&lt;/p&gt;

&lt;p&gt;If you want to see exactly what's in each layer — and why your image weighs what it weighs — &lt;a href="https://github.com/wagoodman/dive" rel="noopener noreferrer"&gt;&lt;code&gt;dive&lt;/code&gt;&lt;/a&gt; is the CLI for that: interactive layer inspection, size per layer, and which files each instruction touched. No Electron, no waiting for an app to load.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;.dockerignore&lt;/code&gt;: the build context matters
&lt;/h2&gt;

&lt;p&gt;You saw &lt;code&gt;.dockerignore&lt;/code&gt; in the last lesson as a way to keep secrets out of your image. There's an equally important reason almost nobody mentions: build speed.&lt;/p&gt;

&lt;p&gt;Think about it. You have a Node.js project. How big is &lt;code&gt;node_modules&lt;/code&gt;? 200 MB? 500 MB? That directory everyone has on their machine and pretends not to look at? The &lt;strong&gt;build context&lt;/strong&gt; is everything Docker packages and sends to the daemon before executing the first instruction — not just what you &lt;code&gt;COPY&lt;/code&gt; into the image, but everything in the directory. Without &lt;code&gt;.dockerignore&lt;/code&gt;, Docker packages and ships all those hundreds of MB on every build, even if they never make it into the image.&lt;/p&gt;

&lt;p&gt;If you've used &lt;a href="https://dev.to/en/courses/mastering-git-from-scratch"&gt;Git&lt;/a&gt;, &lt;code&gt;.dockerignore&lt;/code&gt; works exactly like &lt;code&gt;.gitignore&lt;/code&gt;: same glob syntax, same idea, different destination. Your existing &lt;code&gt;.gitignore&lt;/code&gt; already excludes most of what you don't want in the build context — use it as a starting point and add Docker-specific entries on top.&lt;/p&gt;

&lt;p&gt;A complete &lt;code&gt;.dockerignore&lt;/code&gt; for a Node.js project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="c"&gt;# Dependencies
&lt;/span&gt;&lt;span class="n"&gt;node_modules&lt;/span&gt;
&lt;span class="n"&gt;npm&lt;/span&gt;-&lt;span class="n"&gt;debug&lt;/span&gt;.&lt;span class="n"&gt;log&lt;/span&gt;
&lt;span class="n"&gt;yarn&lt;/span&gt;-&lt;span class="n"&gt;error&lt;/span&gt;.&lt;span class="n"&gt;log&lt;/span&gt;

&lt;span class="c"&gt;# Test coverage
&lt;/span&gt;&lt;span class="n"&gt;coverage&lt;/span&gt;
.&lt;span class="n"&gt;nyc_output&lt;/span&gt;

&lt;span class="c"&gt;# Build output
&lt;/span&gt;&lt;span class="n"&gt;dist&lt;/span&gt;
&lt;span class="n"&gt;build&lt;/span&gt;
.&lt;span class="n"&gt;next&lt;/span&gt;
&lt;span class="n"&gt;out&lt;/span&gt;

&lt;span class="c"&gt;# Environment and secrets
&lt;/span&gt;.&lt;span class="n"&gt;env&lt;/span&gt;
.&lt;span class="n"&gt;env&lt;/span&gt;.&lt;span class="n"&gt;local&lt;/span&gt;
.&lt;span class="n"&gt;env&lt;/span&gt;.*.&lt;span class="n"&gt;local&lt;/span&gt;

&lt;span class="c"&gt;# Git history
&lt;/span&gt;.&lt;span class="n"&gt;git&lt;/span&gt;
.&lt;span class="n"&gt;gitignore&lt;/span&gt;

&lt;span class="c"&gt;# OS files
&lt;/span&gt;.&lt;span class="n"&gt;DS_Store&lt;/span&gt;
&lt;span class="n"&gt;Thumbs&lt;/span&gt;.&lt;span class="n"&gt;db&lt;/span&gt;

&lt;span class="c"&gt;# IDE config
&lt;/span&gt;.&lt;span class="n"&gt;vscode&lt;/span&gt;
.&lt;span class="n"&gt;idea&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And for Python:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="c"&gt;# Virtual environments
&lt;/span&gt;&lt;span class="n"&gt;venv&lt;/span&gt;
.&lt;span class="n"&gt;venv&lt;/span&gt;
&lt;span class="n"&gt;env&lt;/span&gt;

&lt;span class="c"&gt;# Python bytecode
&lt;/span&gt;&lt;span class="err"&gt;__&lt;/span&gt;&lt;span class="n"&gt;pycache__&lt;/span&gt;
*.&lt;span class="n"&gt;pyc&lt;/span&gt;
*.&lt;span class="n"&gt;pyo&lt;/span&gt;
*.&lt;span class="n"&gt;pyd&lt;/span&gt;

&lt;span class="c"&gt;# Test coverage
&lt;/span&gt;.&lt;span class="n"&gt;pytest_cache&lt;/span&gt;
&lt;span class="n"&gt;htmlcov&lt;/span&gt;
.&lt;span class="n"&gt;coverage&lt;/span&gt;

&lt;span class="c"&gt;# Build artifacts
&lt;/span&gt;&lt;span class="n"&gt;dist&lt;/span&gt;
&lt;span class="n"&gt;build&lt;/span&gt;
*.&lt;span class="n"&gt;egg&lt;/span&gt;-&lt;span class="n"&gt;info&lt;/span&gt;

&lt;span class="c"&gt;# Git
&lt;/span&gt;.&lt;span class="n"&gt;git&lt;/span&gt;

&lt;span class="c"&gt;# Environment
&lt;/span&gt;.&lt;span class="n"&gt;env&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The rule: if Docker doesn't need that file to build the image, don't send it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choosing the right base image
&lt;/h2&gt;

&lt;p&gt;Your &lt;code&gt;FROM&lt;/code&gt; choice determines your image's starting size, the vulnerabilities it inherits, and how long it takes to pull. A decision that looks trivial with consequences that aren't.&lt;/p&gt;

&lt;p&gt;The most common Python variants — and the logic applies to any runtime:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;python:3.12         → ~1.02 GB   (full Debian, everything included)
python:3.12-slim    →  ~148 MB   (Debian without extras)
python:3.12-alpine  →   ~57 MB   (Alpine Linux, minimum possible)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The temptation is to go straight for Alpine because it's the smallest. This is where things get genuinely weird.&lt;/p&gt;

&lt;p&gt;Alpine uses &lt;code&gt;musl libc&lt;/code&gt; instead of &lt;code&gt;glibc&lt;/code&gt;. Most of the time that's invisible. But Python libraries with C extensions — &lt;code&gt;numpy&lt;/code&gt;, &lt;code&gt;pandas&lt;/code&gt;, &lt;code&gt;Pillow&lt;/code&gt;, basically everything that does anything useful — don't have precompiled wheels for Alpine. So what does pip do? Compiles them from source. During the build. Along with all the headers and tooling that requires installing first.&lt;/p&gt;

&lt;p&gt;The result: you picked the smallest base image to be efficient, and your build now takes 10 minutes instead of 30 seconds. You optimized for size and got the opposite. That's the Alpine tax that every "reduce your Docker image size" tutorial conveniently leaves out.&lt;/p&gt;

&lt;p&gt;The practical recommendation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;-slim&lt;/code&gt;&lt;/strong&gt;: the sensible starting point for most projects. Significantly smaller than full, without Alpine's compatibility headaches.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;-alpine&lt;/code&gt;&lt;/strong&gt;: for simple tool images, Go binaries, or when you've verified there are no incompatible dependencies.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Full (no suffix)&lt;/strong&gt;: only when you need specific Debian build tools you can't install yourself.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One more thing: &lt;strong&gt;pin your version&lt;/strong&gt;. Don't use &lt;code&gt;python:3.12-slim&lt;/code&gt;; use &lt;code&gt;python:3.12.10-slim&lt;/code&gt;. Floating tags can silently change with a &lt;code&gt;docker pull&lt;/code&gt;. In production, that kind of surprise tends to happen at the worst possible moment.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# ❌ Floating tag — can change without warning&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; python:3.12-slim&lt;/span&gt;

&lt;span class="c"&gt;# ✅ Pinned version — reproducible everywhere&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; python:3.12.10-slim-bookworm&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There's more to explore — distroless images from Google, Chainguard images, BuildKit's parallel execution. But what's in this lesson already gets you fast builds and lean images for real-world projects.&lt;/p&gt;




&lt;p&gt;Layer cache with the right ordering, combined &lt;code&gt;RUN&lt;/code&gt; instructions, a solid &lt;code&gt;.dockerignore&lt;/code&gt;, and a sensible base image choice. Four practices that directly affect the time you spend staring at a terminal.&lt;/p&gt;

&lt;p&gt;In the next lesson, we get to the part everyone tends to ignore until it's too late: &lt;strong&gt;Dockerfile security best practices&lt;/strong&gt;. We'll cover why running as root inside a container is a terrible idea, how to scan for vulnerabilities, and how to handle secrets without baking them into your image for eternity.&lt;/p&gt;

&lt;p&gt;Never stop coding!&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;💡 Challenge&lt;/strong&gt;: Take the Dockerfile from the previous lesson's challenge (the Flask app). Change one line of app code and rebuild with &lt;code&gt;docker build&lt;/code&gt;. Watch which layers rebuild. Then reorder the Dockerfile so dependency installation gets cached correctly, rebuild again with a code change, and compare the times. The &lt;code&gt;docker build&lt;/code&gt; output will tell you exactly what hit cache and what didn't.&lt;/p&gt;

</description>
      <category>docker</category>
      <category>beginners</category>
      <category>tutorial</category>
      <category>devops</category>
    </item>
    <item>
      <title>Git commit best practices</title>
      <dc:creator>Javi Palacios</dc:creator>
      <pubDate>Fri, 14 Aug 2026 09:43:32 +0000</pubDate>
      <link>https://dev.to/fj_palacios/git-commit-best-practices-2dbl</link>
      <guid>https://dev.to/fj_palacios/git-commit-best-practices-2dbl</guid>
      <description>&lt;p&gt;There's a particular kind of dread that only developers know: running &lt;code&gt;git log --oneline&lt;/code&gt; on a project you wrote two years ago and finding this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;a1b2c3d fix
e4f5g6h wip
i7j8k9l more stuff
m0n1o2p trying something
q3r4s5t FINAL
u6v7w8x FINAL_REAL_THIS_TIME
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The bad part isn't seeing it. The bad part is recognizing the handwriting. You wrote those commits. In a period of your life when you apparently had better things to think about.&lt;/p&gt;

&lt;p&gt;The problem isn't aesthetic — it's functional. When you run &lt;code&gt;git bisect&lt;/code&gt; three months from now to track down a regression and Git points at &lt;code&gt;q3r4s5t FINAL_REAL_THIS_TIME&lt;/code&gt; as the culprit, the history will tell you nothing. You'll have to read the full diff line by line to figure out what changed, why, and whether it was intentional. Repeated for every commit in the range. At 2 AM.&lt;/p&gt;

&lt;p&gt;A well-written commit history is living documentation. This lesson is about how to write one.&lt;/p&gt;

&lt;h2&gt;
  
  
  The atomic commit principle
&lt;/h2&gt;

&lt;p&gt;Before talking about format, the most important idea: each commit should contain &lt;strong&gt;exactly one logical change&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Not "Tuesday's work." Not "everything from the sprint." One change, well-scoped, with its context.&lt;/p&gt;

&lt;p&gt;Why does this matter so much?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;git bisect&lt;/code&gt; works&lt;/strong&gt;: each commit is independently verifiable. A commit that mixes five changes tells you nothing when bisect points at it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;git revert&lt;/code&gt; is painless&lt;/strong&gt;: undoing an atomic commit is clean. Undoing one that mixes a refactor, a migration, and a CSS fix is not.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;git cherry-pick&lt;/code&gt; becomes possible&lt;/strong&gt;: you can move a specific change to another branch without dragging unrelated things along.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Code review is faster&lt;/strong&gt;: the reviewer sees a single, clear unit. They don't have to guess which lines belong together.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The temptation is to group things because they're "related." But "related" is too broad. A function refactor and the tests that cover it — fine, those can go together. A refactor, a database migration, and a CSS fix — those are three separate commits.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conventional Commits
&lt;/h2&gt;

&lt;p&gt;Once you're clear on what goes in each commit, you need to decide how to describe it. &lt;a href="https://www.conventionalcommits.org" rel="noopener noreferrer"&gt;Conventional Commits&lt;/a&gt; is a lightweight specification for commit message format. Projects like Angular, Vue, Vite, and thousands of open source libraries use it — not for aesthetics, but because the format is machine-readable. Tools like &lt;code&gt;semantic-release&lt;/code&gt; can generate changelogs and compute the next version automatically based on your commit types.&lt;/p&gt;

&lt;p&gt;The format:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;type(scope): subject

body

footer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A real example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;feat(auth): Add Bearer token validation

The authorization header now requires the Bearer prefix.
Bare tokens sent without it will receive a 401 response.

BREAKING CHANGE: Authorization header format changed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That looks like a lot of structure for a text message. Bear with me — each part has a concrete reason.&lt;/p&gt;

&lt;h2&gt;
  
  
  Types
&lt;/h2&gt;

&lt;p&gt;The type describes what kind of change the commit contains. The most common ones:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;When to use&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;feat&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;New functionality&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;fix&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Bug fix&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;docs&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Documentation changes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;refactor&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Refactoring without observable behavior change&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;test&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Adding or fixing tests&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;chore&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Maintenance: dependencies, configs, scripts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;perf&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Performance improvement&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;style&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Formatting, whitespace, semicolons — no logic change&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ci&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;CI/CD configuration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;build&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Build system changes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;revert&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Reverting a previous commit&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Don't stress about memorizing the full list from day one. &lt;code&gt;feat&lt;/code&gt;, &lt;code&gt;fix&lt;/code&gt;, &lt;code&gt;refactor&lt;/code&gt;, and &lt;code&gt;chore&lt;/code&gt; cover more than 80% of day-to-day commits. The others you pick up when you need them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scope (optional)
&lt;/h2&gt;

&lt;p&gt;The scope goes in parentheses after the type and indicates the area of code affected:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;feat(auth): Add token refresh endpoint
fix(api): Return 404 for deleted users
refactor(database): Extract query builder to its own module
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There's no canonical list of correct scopes — each project defines its own. What matters is &lt;strong&gt;consistency&lt;/strong&gt;: if the auth layer is called &lt;code&gt;auth&lt;/code&gt;, always call it &lt;code&gt;auth&lt;/code&gt;, not &lt;code&gt;authentication&lt;/code&gt; one day and &lt;code&gt;auth&lt;/code&gt; the next. Scopes are useful for filtering history; they lose that value if they're not stable.&lt;/p&gt;

&lt;p&gt;If the change is cross-cutting or doesn't fit neatly into any area, just leave it out.&lt;/p&gt;

&lt;h2&gt;
  
  
  The subject line
&lt;/h2&gt;

&lt;p&gt;The subject is the first line — what you see in &lt;code&gt;git log --oneline&lt;/code&gt;. It's the most important part of the message:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;feat(auth): Add Bearer token validation
             ↑
             imperative mood — "Add", not "Added" or "Adding"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Imperative mood&lt;/strong&gt;: "Add feature", "Fix null check", "Refactor validation". Not "Added", not "Adding", not "Fixes". Git itself uses this style in its own messages (&lt;code&gt;Merge branch&lt;/code&gt;, &lt;code&gt;Revert "feat: ..."&lt;/code&gt;). Matching it is consistency, not arbitrary convention.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;70 characters max&lt;/strong&gt;: GitHub and GitLab truncate the subject line in their interfaces beyond that. Anything longer belongs in the body.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No trailing period.&lt;/strong&gt; &lt;strong&gt;Capitalize the first letter.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;And most importantly: &lt;strong&gt;describe the change, not the activity&lt;/strong&gt;. &lt;code&gt;feat(auth): Add null check before token verification&lt;/code&gt; is useful. &lt;code&gt;fix: Fix bug&lt;/code&gt; is useful to no one.&lt;/p&gt;

&lt;h2&gt;
  
  
  The body
&lt;/h2&gt;

&lt;p&gt;The body is optional, but it's where the most durable value lives: the &lt;strong&gt;why&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;It's separated from the subject by a blank line (required — Git treats them as distinct sections without it):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;fix(api): Handle null response in user endpoint

Accounts deleted via the legacy migration tool can leave ghost sessions
in the database. The endpoint was crashing on these instead of
returning a clean 404.

The migration tool will be deprecated in Q3, but until then this
null check is necessary.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The diff already shows what changed. The body explains the context the diff can never show: why the bug existed, what scenario triggered it, what alternatives were ruled out, what's expected to change later. That information disappears forever if you don't write it down now — you're the only one with the full context, and you have it right now, while making the commit.&lt;/p&gt;

&lt;p&gt;Not every commit needs a body. &lt;code&gt;chore: Update dependencies&lt;/code&gt; doesn't need elaboration. But for a non-obvious fix, a design decision, or a change that might surprise someone six months from now, the body is invaluable. Future you will thank present you. Or at least stop blaming present you.&lt;/p&gt;

&lt;h2&gt;
  
  
  The footer and breaking changes
&lt;/h2&gt;

&lt;p&gt;The footer is for structured metadata: issue references and, most importantly, breaking changes.&lt;/p&gt;

&lt;p&gt;A &lt;strong&gt;breaking change&lt;/strong&gt; is a change that breaks backward compatibility with a previous API or public behavior. It's declared in two complementary ways:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;feat(api)!: Remove deprecated v1 endpoints

All v1 endpoints removed. Clients must migrate to the v2 API.

BREAKING CHANGE: v1 endpoints no longer available
Closes #341
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;!&lt;/code&gt; after the type is the fast signal — visible at a glance in the log. The &lt;code&gt;BREAKING CHANGE:&lt;/code&gt; in the footer is the formal declaration that release tools use to know a major version bump is needed in semver. Both together ensure both humans and tooling understand the magnitude of the change.&lt;/p&gt;

&lt;p&gt;Issue references also go in the footer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;fix(auth): Reject expired tokens on silent refresh

Closes #482
Refs #391
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The practical workflow: --fixup and --autosquash
&lt;/h2&gt;

&lt;p&gt;The atomic commit principle collides with reality in a predictable way: you make a commit, keep working, and realize there's an error in that previous commit. The instinctive solution is a new commit that says "fix typo" or "oops." The correct solution is &lt;code&gt;--fixup&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;&lt;span class="c"&gt;# You realize commit abc123 needs a correction&lt;/span&gt;
git add &lt;span class="nt"&gt;-p&lt;/span&gt;                    &lt;span class="c"&gt;# stage only the fix&lt;/span&gt;
git commit &lt;span class="nt"&gt;--fixup&lt;/span&gt; abc123     &lt;span class="c"&gt;# creates: "fixup! feat(auth): Add Bearer..."&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This creates a commit automatically marked as a fixup for the original. When you clean up history before merging:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git rebase &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="nt"&gt;--autosquash&lt;/span&gt; main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Git reorganizes the commits on its own and squashes the fixups into their target commits. What lands in &lt;code&gt;main&lt;/code&gt; is clean: no "fix typo", no "oops", no "more stuff." The project history, told in order.&lt;/p&gt;




&lt;p&gt;A well-written commit history isn't extra documentation someone decided to maintain — it's the documentation Git gives you for free if you feed it properly. A year from now, when someone runs &lt;code&gt;git log -S "mysterious_function"&lt;/code&gt; to understand why that function exists, your commits are the only thing they'll find. Make them worth finding.&lt;/p&gt;

&lt;p&gt;In the next tutorial we cover branch naming conventions: how to name branches so your team understands at a glance what they contain, which ticket they're tied to, and when it's safe to delete them.&lt;/p&gt;




&lt;p&gt;💡 &lt;strong&gt;Challenge&lt;/strong&gt;: Find the most cryptic commit in one of your projects. Rewrite it in Conventional Commits format with a proper subject, body, and footer if applicable. The exercise trains the muscle for writing the message in the moment, not reconstructing it after the fact.&lt;/p&gt;

&lt;p&gt;Never stop coding!&lt;/p&gt;

</description>
      <category>git</category>
      <category>beginners</category>
      <category>tutorial</category>
      <category>devops</category>
    </item>
    <item>
      <title>How to resolve merge conflicts in Git</title>
      <dc:creator>Javi Palacios</dc:creator>
      <pubDate>Thu, 13 Aug 2026 11:07:50 +0000</pubDate>
      <link>https://dev.to/fj_palacios/how-to-resolve-merge-conflicts-in-git-3862</link>
      <guid>https://dev.to/fj_palacios/how-to-resolve-merge-conflicts-in-git-3862</guid>
      <description>&lt;p&gt;You run &lt;code&gt;git merge feature/auth-improvements&lt;/code&gt; and instead of the clean one-liner you were hoping for, your terminal dumps this:&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="go"&gt;Auto-merging src/auth/middleware.ts
CONFLICT (content): Merge conflict in src/auth/middleware.ts
&lt;/span&gt;&lt;span class="gp"&gt;Automatic merge failed;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;fix conflicts and &lt;span class="k"&gt;then &lt;/span&gt;commit the result.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You open the file and find:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;HEAD&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;authorization&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt; &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Unauthorized&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="o"&gt;=======&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;authHeader&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;authorization&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;authHeader&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;authHeader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Bearer &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;authHeader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt; &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;No token provided&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;feature&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;improvements&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What is &lt;code&gt;HEAD&lt;/code&gt; here? Which version is "correct"? Do you pick one and delete the other, or can you combine them? What happens if you accidentally leave a &lt;code&gt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&lt;/code&gt; in the file?&lt;/p&gt;

&lt;p&gt;Conflict markers have a logic to them. Once you understand it, resolving conflicts stops being stressful and becomes something you can do methodically — still a bit tedious, but never mysterious.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conflict markers
&lt;/h2&gt;

&lt;p&gt;When Git can't automatically resolve a merge, it marks the conflicting sections with a syntax that's been unchanged for decades (yes, those three angle brackets — it's genuinely one of the least intuitive choices in software tooling, and yet here we are). The structure is always the same:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt; HEAD                          ← start of your version (current branch)
your version of the code
=======                               ← separator
their version of the code
&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt; feature/auth-improvements     ← end of their version (branch being merged)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;HEAD&lt;/code&gt; is your current branch — what you have right now. The label after &lt;code&gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&lt;/code&gt; is the name of the branch you're merging in. The &lt;code&gt;=======&lt;/code&gt; is the divider between the two versions.&lt;/p&gt;

&lt;p&gt;To resolve the conflict you have three choices: keep your version (top), keep theirs (bottom), or write a new version that integrates both. What you cannot do is leave the markers in the file — that's not valid code, and it will cause problems the moment someone runs it.&lt;/p&gt;

&lt;h3&gt;
  
  
  The diff3 variant
&lt;/h3&gt;

&lt;p&gt;There's a setting that gives you significantly more context, and you should enable it now:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git config &lt;span class="nt"&gt;--global&lt;/span&gt; merge.conflictstyle diff3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With &lt;code&gt;diff3&lt;/code&gt;, the markers include an extra section: the &lt;strong&gt;common ancestor&lt;/strong&gt; — what the file looked like before either of you touched it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;HEAD&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;authorization&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt; &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Unauthorized&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="o"&gt;|||||||&lt;/span&gt; &lt;span class="nx"&gt;merged&lt;/span&gt; &lt;span class="nx"&gt;common&lt;/span&gt; &lt;span class="nx"&gt;ancestors&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;x-auth-token&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;403&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Forbidden&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="o"&gt;=======&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;authHeader&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;authorization&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;authHeader&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;authHeader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Bearer &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;authHeader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt; &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;No token provided&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;feature&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;improvements&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now you can see what each person actually changed. The &lt;code&gt;|||||||&lt;/code&gt; section is the original. From that, it's clear: you switched from &lt;code&gt;x-auth-token&lt;/code&gt; to &lt;code&gt;authorization&lt;/code&gt;, and they also switched the header but added Bearer format validation on top. The right resolution isn't picking a winner — it's combining both changes. That's obvious once you see the base. Without it, you're guessing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three-way merge
&lt;/h2&gt;

&lt;p&gt;The reason &lt;code&gt;diff3&lt;/code&gt; makes sense becomes clear when you understand how a merge actually works.&lt;/p&gt;

&lt;p&gt;When you merge two branches, Git doesn't just compare your version against theirs. It compares &lt;strong&gt;both versions against the common ancestor&lt;/strong&gt; — the last commit both branches share. That's why it's called a three-way merge: three versions, three ways.&lt;/p&gt;

&lt;p&gt;The logic is clean: if only one side changed a line, Git resolves it automatically — it takes the changed version, because the other side didn't do anything different. A conflict only appears when &lt;strong&gt;both sides&lt;/strong&gt; changed the same area of code in different ways. Git doesn't know which change was intentional, so it stops and asks you.&lt;/p&gt;

&lt;p&gt;Without the common ancestor, you can't tell whether your version should "win" or whether you need to integrate both changes. With it, you have all the information needed to make the right call.&lt;/p&gt;

&lt;h2&gt;
  
  
  Resolving conflicts by hand
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;git status&lt;/code&gt; tells you exactly which files have conflicts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git status
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;On branch main
You have unmerged paths.
  &lt;span class="o"&gt;(&lt;/span&gt;fix conflicts and run &lt;span class="s2"&gt;"git commit"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
  &lt;span class="o"&gt;(&lt;/span&gt;use &lt;span class="s2"&gt;"git merge --abort"&lt;/span&gt; to abort the merge&lt;span class="o"&gt;)&lt;/span&gt;

Unmerged paths:
  &lt;span class="o"&gt;(&lt;/span&gt;use &lt;span class="s2"&gt;"git add &amp;lt;file&amp;gt;..."&lt;/span&gt; to mark resolution&lt;span class="o"&gt;)&lt;/span&gt;
        both modified:   src/auth/middleware.ts
        both modified:   src/config/database.ts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The process for each conflicted file:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open it and find the &lt;code&gt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&lt;/code&gt; markers&lt;/li&gt;
&lt;li&gt;Decide what the code should look like (yours, theirs, or a blend)&lt;/li&gt;
&lt;li&gt;Delete &lt;strong&gt;all&lt;/strong&gt; the markers (&lt;code&gt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&lt;/code&gt;, &lt;code&gt;|||||||&lt;/code&gt;, &lt;code&gt;=======&lt;/code&gt;, &lt;code&gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Save the file&lt;/li&gt;
&lt;li&gt;&lt;code&gt;git add src/auth/middleware.ts&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Once every conflicted file is resolved and staged:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git commit
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Git has a merge commit message already prepared. Edit it or leave it as-is.&lt;/p&gt;

&lt;h3&gt;
  
  
  For entire files: --ours and --theirs
&lt;/h3&gt;

&lt;p&gt;If there's a file where you know one version wins completely — no blending needed — you don't have to edit markers manually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git checkout &lt;span class="nt"&gt;--ours&lt;/span&gt; src/package-lock.json    &lt;span class="c"&gt;# take our version entirely&lt;/span&gt;
git checkout &lt;span class="nt"&gt;--theirs&lt;/span&gt; src/yarn.lock          &lt;span class="c"&gt;# take their version entirely&lt;/span&gt;
git add src/package-lock.json src/yarn.lock
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is especially useful for auto-generated files like lockfiles. Merging two lockfiles line by line is a losing battle — the correct lockfile is the one that results from running your package manager in your environment, not some hybrid of two different install states.&lt;/p&gt;

&lt;h2&gt;
  
  
  Merge tools
&lt;/h2&gt;

&lt;p&gt;Resolving conflicts manually in large files is doable but slow. Merge tools give you a visual layout of the conflicting versions — no more scrolling through markers trying to keep track of which block belongs to whom.&lt;/p&gt;

&lt;h3&gt;
  
  
  lazygit
&lt;/h3&gt;

&lt;p&gt;If you live in the terminal — or if the idea of opening VS Code to resolve four conflict hunks feels like calling the fire department because your toast burned —, &lt;code&gt;lazygit&lt;/code&gt; is the tool. A full TUI (terminal UI) for Git: no Electron, no three-second startup, no fan suddenly deciding this is its moment to shine.&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="c"&gt;# Install lazygit&lt;/span&gt;
brew &lt;span class="nb"&gt;install &lt;/span&gt;lazygit          &lt;span class="c"&gt;# macOS / Linux via Homebrew&lt;/span&gt;
pacman &lt;span class="nt"&gt;-S&lt;/span&gt; lazygit             &lt;span class="c"&gt;# Arch (nobody's surprised)&lt;/span&gt;
apt &lt;span class="nb"&gt;install &lt;/span&gt;lazygit           &lt;span class="c"&gt;# Debian/Ubuntu&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open &lt;code&gt;lazygit&lt;/code&gt; in the repository and conflicted files appear clearly marked in the &lt;strong&gt;Files&lt;/strong&gt; panel. Select one and the right panel shows the conflict hunks: you can navigate between them, pick your version or theirs with a single keypress, or drop into your &lt;code&gt;$EDITOR&lt;/code&gt; for anything that needs a more surgical approach. When you're done, the file is ready to stage — without opening a new window or switching context.&lt;/p&gt;

&lt;p&gt;For anyone who resolves conflicts regularly, the speed difference is real. Not because lazygit is magic, but because there's nothing to wait for.&lt;/p&gt;

&lt;h3&gt;
  
  
  VS Code and IntelliJ
&lt;/h3&gt;

&lt;p&gt;For those who prefer a full GUI with a three-panel view (your version, the ancestor, theirs), VS Code and IntelliJ IDEA have built-in merge support. To configure them for use with &lt;code&gt;git mergetool&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;git config &lt;span class="nt"&gt;--global&lt;/span&gt; merge.tool vscode
git config &lt;span class="nt"&gt;--global&lt;/span&gt; mergetool.vscode.cmd &lt;span class="s1"&gt;'code --wait $MERGED'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git mergetool
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Git opens each conflicted file in your configured tool, one at a time. Works well for complex conflicts where you want to see a full file's worth of context on a wide screen.&lt;/p&gt;

&lt;h3&gt;
  
  
  vimdiff
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;vimdiff&lt;/code&gt; is available without any extra configuration. Arch users already know what comes next; everyone else discovers it by accident on a day when nothing else is installed, learns more Vim shortcuts than they planned, and walks away with an ambiguous relationship with the tool. It works. It's not exactly friendly. But it's been working for decades, which counts for something.&lt;/p&gt;

&lt;p&gt;And if the accidental session leaves you with more questions than answers — or just stuck in a buffer with no idea how to exit — &lt;a href="https://dev.to/en/courses/mastering-vim-from-scratch"&gt;there's a Vim course from scratch&lt;/a&gt; that starts exactly from there.&lt;/p&gt;

&lt;h2&gt;
  
  
  git merge --abort
&lt;/h2&gt;

&lt;p&gt;Sometimes the right move is to back out entirely.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git merge &lt;span class="nt"&gt;--abort&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This undoes the merge completely and puts you back exactly where you were before running &lt;code&gt;git merge&lt;/code&gt;. No trace, no half-committed state, no stray markers in the code. As if it never happened.&lt;/p&gt;

&lt;p&gt;When to use it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The conflicts are too extensive and you need to rethink your approach before continuing&lt;/li&gt;
&lt;li&gt;You merged the wrong branch&lt;/li&gt;
&lt;li&gt;You need to sync with your team before making decisions about conflicting code&lt;/li&gt;
&lt;li&gt;Your merge tool crashed halfway through (it happens more than it should)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's not giving up. It's knowing when to stop so you can do it right, rather than forcing a resolution you're not sure about.&lt;/p&gt;

&lt;h2&gt;
  
  
  Strategies for complex conflicts
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Let one side always win
&lt;/h3&gt;

&lt;p&gt;If you know that in case of a conflict your version should always take precedence (or always theirs), you can tell Git upfront:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git merge &lt;span class="nt"&gt;-X&lt;/span&gt; ours feature/branch    &lt;span class="c"&gt;# on conflict: take our version&lt;/span&gt;
git merge &lt;span class="nt"&gt;-X&lt;/span&gt; theirs feature/branch  &lt;span class="c"&gt;# on conflict: take their version&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This only kicks in when there's a real conflict — it doesn't touch changes Git can resolve automatically. Useful for maintenance branch merges where the precedence rule is clear before you start.&lt;/p&gt;

&lt;h3&gt;
  
  
  Rebase first, then merge
&lt;/h3&gt;

&lt;p&gt;Conflicts are less frequent — and when they do appear, smaller and with clearer context — when your branch is up to date with &lt;code&gt;main&lt;/code&gt;. Instead of merging directly after weeks of divergence, sync first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git fetch origin
git rebase origin/main
&lt;span class="c"&gt;# resolve conflicts one commit at a time if they appear&lt;/span&gt;
git push &lt;span class="nt"&gt;--force-with-lease&lt;/span&gt; origin feature/your-branch
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rebase replays your commits on top of the updated &lt;code&gt;main&lt;/code&gt;. If conflicts appear, you deal with them one commit at a time — small doses, each with clear context about what changed in that specific step. Far more manageable than a single merge with weeks of accumulated divergence.&lt;/p&gt;

&lt;h2&gt;
  
  
  Preventing conflicts
&lt;/h2&gt;

&lt;p&gt;The best resolution is the one you don't have to do.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sync frequently.&lt;/strong&gt; A branch that hasn't touched &lt;code&gt;main&lt;/code&gt; in three weeks will have conflicts. A daily &lt;code&gt;git rebase origin/main&lt;/code&gt; keeps the gap small and the conflicts manageable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Small, short-lived branches.&lt;/strong&gt; The longer a branch lives, the more it diverges. Long features get broken into smaller pieces that merge early, before the differences have time to accumulate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Talk to your team before touching critical files.&lt;/strong&gt; Database migrations, &lt;code&gt;package.json&lt;/code&gt;, shared configuration files — these are conflict hotspots. It's not a technical problem; it's a communication problem. "I'm going to change the user schema this afternoon — does that affect anyone?" in the team channel prevents half an hour of conflict resolution.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Feature flags instead of long-lived branches.&lt;/strong&gt; If the code is deployed but turned off by a flag, you can merge without waiting for the feature to be finished. The branch lives days instead of weeks, and conflicts don't have time to build up.&lt;/p&gt;




&lt;p&gt;Conflicts are the part of collaborative work that Git delegates to humans — and rightly so, because the machine doesn't know which change was intentional. Understanding the three-way merge, enabling &lt;code&gt;diff3&lt;/code&gt;, and knowing when to reach for &lt;code&gt;--abort&lt;/code&gt; instead of forcing through a resolution you don't fully understand: those three things turn something intimidating into something completely manageable.&lt;/p&gt;

&lt;p&gt;In the next tutorial we start the workflows module: how professional teams organize their day-to-day Git usage, from the classic Gitflow model to trunk-based development, and when each one makes sense.&lt;/p&gt;




&lt;p&gt;💡 &lt;strong&gt;Challenge&lt;/strong&gt;: Create two branches from the same commit, modify the same lines of a file in each branch in different ways, then merge one into the other. Enable &lt;code&gt;diff3&lt;/code&gt; first if you haven't already, and pay attention to how the common ancestor helps you understand exactly what each side changed.&lt;/p&gt;

&lt;p&gt;Never stop coding!&lt;/p&gt;

</description>
      <category>git</category>
      <category>beginners</category>
      <category>tutorial</category>
      <category>devops</category>
    </item>
    <item>
      <title>Investigating Code Changes Line by Line with git blame</title>
      <dc:creator>Javi Palacios</dc:creator>
      <pubDate>Wed, 12 Aug 2026 07:53:15 +0000</pubDate>
      <link>https://dev.to/fj_palacios/investigating-code-changes-line-by-line-with-git-blame-nbk</link>
      <guid>https://dev.to/fj_palacios/investigating-code-changes-line-by-line-with-git-blame-nbk</guid>
      <description>&lt;p&gt;You've been there. You're staring at a file with a 50-line function that makes zero sense. Nobody on the team knows what it does. The person who wrote it left the company. And the commit says "WIP". Or "fix". Or — my personal favorite — "asdfgh".&lt;/p&gt;

&lt;p&gt;Who wrote this? When? Was anyone even reviewing code at this point?&lt;/p&gt;

&lt;p&gt;This is where &lt;code&gt;git blame&lt;/code&gt; goes from a useful tool to your best ally for code archaeology (and possibly the most hated person in your team's history).&lt;/p&gt;

&lt;h2&gt;
  
  
  git blame line by line
&lt;/h2&gt;

&lt;p&gt;The name is dramatic on purpose. It tells you exactly who added each line, in which commit, and when. There's no hiding from it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git blame file.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output gives you everything you need:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;abcdef1 (Maria Gomez 2026-03-15 10:23 +0200  1) def calculate_total(items):
bcdef12 (Maria Gomez 2026-03-15 10:23 +0200  2)     total = 0
1234567 (John Perez  2026-04-02 14:05 +0200  3)     for item in items:
abcdef1 (Maria Gomez 2026-03-15 10:23 +0200  4)         total += item.price
defa456 (Maria Gomez 2026-04-05 16:41 +0200  5)     return total * 1.21  # TODO: hardcoded tax
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each line shows: commit hash, author, date and time, line number, and content.&lt;/p&gt;

&lt;p&gt;You spot the &lt;code&gt;# TODO: hardcoded tax&lt;/code&gt; on line 5, grab the hash &lt;code&gt;defa456&lt;/code&gt;, and run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git show defa456
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Full commit right there: what changed, why, and in what context. Next time someone says "that code was always there", you've got the exact timestamp.&lt;/p&gt;

&lt;h3&gt;
  
  
  Blame on a line range
&lt;/h3&gt;

&lt;p&gt;If the file has 800 lines, you don't need blame for all of them. Limit it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git blame &lt;span class="nt"&gt;-L&lt;/span&gt; 45,90 file.py     &lt;span class="c"&gt;# lines 45 to 90&lt;/span&gt;
git blame &lt;span class="nt"&gt;-L&lt;/span&gt; 45,+20 file.py    &lt;span class="c"&gt;# 20 lines starting from line 45&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Much more manageable when you already know where the problem is.&lt;/p&gt;

&lt;h2&gt;
  
  
  Filtering the noise: -w and -M
&lt;/h2&gt;

&lt;p&gt;By default, &lt;code&gt;git blame&lt;/code&gt; shows the author of the last person who touched that line. But if someone reformatted the file — indentation, whitespace — it blames the wrong person. That leads to unnecessary awkward conversations.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git blame &lt;span class="nt"&gt;-w&lt;/span&gt; file.py       &lt;span class="c"&gt;# ignore whitespace changes&lt;/span&gt;
git blame &lt;span class="nt"&gt;-M&lt;/span&gt; file.py       &lt;span class="c"&gt;# detect lines moved within the file&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;-M&lt;/code&gt; flag is especially useful after big refactors where someone shuffled functions around the file. It shows the original author, not the one who did the internal copy-paste.&lt;/p&gt;

&lt;h2&gt;
  
  
  git log -p: history with full diffs
&lt;/h2&gt;

&lt;p&gt;Sometimes you don't care who touched what. You want to see ALL the changes in a file over time, with the complete diff in every commit — without running &lt;code&gt;git show&lt;/code&gt; for each one manually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git log &lt;span class="nt"&gt;-p&lt;/span&gt; file.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Navigate with the arrow keys, exit with &lt;code&gt;:q&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Don't stress if the output is massive — &lt;code&gt;git log&lt;/code&gt; has filtering options for exactly this, and we'll cover them next.&lt;/p&gt;

&lt;h2&gt;
  
  
  git log --follow: when the file was renamed
&lt;/h2&gt;

&lt;p&gt;A file can be renamed several times over its life. &lt;code&gt;git log file.py&lt;/code&gt; shows its history, but if it used to be called &lt;code&gt;old_file.py&lt;/code&gt;, it won't see that.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git log &lt;span class="nt"&gt;--follow&lt;/span&gt; file.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Git follows the file even after name changes. You get the complete history — what the file was called at every point in its life.&lt;/p&gt;

&lt;p&gt;(On Windows, filesystems are case-insensitive by default — you know how this goes — so if someone renamed &lt;code&gt;Auth.py&lt;/code&gt; to &lt;code&gt;auth.py&lt;/code&gt;, Git loses the trail without this flag.)&lt;/p&gt;

&lt;h2&gt;
  
  
  git log -S: find when a piece of code appeared
&lt;/h2&gt;

&lt;p&gt;This is the most powerful option when you know &lt;em&gt;what&lt;/em&gt; you're looking for but not &lt;em&gt;when&lt;/em&gt; it appeared.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git log &lt;span class="nt"&gt;-S&lt;/span&gt; &lt;span class="s2"&gt;"calculate_total"&lt;/span&gt; &lt;span class="nt"&gt;--oneline&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Shows all commits that added or removed that string from the code. It doesn't search commit messages — it searches the actual diff. If you're like me when I started, this feels like dark magic. It isn't, but it's close.&lt;/p&gt;

&lt;p&gt;Perfect for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"When was this bug introduced?"&lt;/li&gt;
&lt;li&gt;"This variable isn't used anymore — when did it stop being used?"&lt;/li&gt;
&lt;li&gt;"Who added this dependency without telling anyone?"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Combine with &lt;code&gt;-p&lt;/code&gt; to see the full context in each commit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git log &lt;span class="nt"&gt;-S&lt;/span&gt; &lt;span class="s2"&gt;"calculate_total"&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: &lt;code&gt;-S&lt;/code&gt; searches for an exact string and detects when the occurrence count changes. If you need regex, use &lt;code&gt;-G&lt;/code&gt; instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  git show: see a file at any point in time
&lt;/h2&gt;

&lt;p&gt;Once you've got a hash — from blame, log, or anywhere — looking at the full commit is trivial:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git show abc1234
git show abc1234:src/app.py        &lt;span class="c"&gt;# the file as it was at that commit&lt;/span&gt;
git show abc1234 &lt;span class="nt"&gt;--stat&lt;/span&gt;           &lt;span class="c"&gt;# changes summary, no full diff&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;git show hash:path/to/file&lt;/code&gt; is especially useful when you need to see what a file looked like at a specific point without having to checkout. See it, copy what you need, move on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical workflow
&lt;/h2&gt;

&lt;p&gt;You're new to a codebase. You find a weird function. You want to know its story. Here's the process:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Who added it?&lt;/strong&gt; &lt;code&gt;git blame file.py&lt;/code&gt; → grab the hash from the suspicious lines&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What changed in that commit?&lt;/strong&gt; &lt;code&gt;git show hash&lt;/code&gt; → full context&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;When did it appear?&lt;/strong&gt; &lt;code&gt;git log -S "function_name"&lt;/code&gt; → complete timeline&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Was the file renamed?&lt;/strong&gt; &lt;code&gt;git log --follow file.py&lt;/code&gt; → full history including renames&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Four commands for a complete code forensics investigation. After this, "I don't know who wrote that" stops being a valid answer on your team.&lt;/p&gt;

&lt;h2&gt;
  
  
  For the next tutorial
&lt;/h2&gt;

&lt;p&gt;Next up is what you need when things get serious: merges. Merge conflicts, strategies to resolve them, and how to avoid them by just talking to your team before they happen. That last one sounds obvious. It isn't.&lt;/p&gt;




&lt;p&gt;💡 &lt;strong&gt;Challenge&lt;/strong&gt;: Pick a file in your current project you don't know well and use the four commands in this tutorial to reconstruct its history. You'll find things you didn't expect. Probably things you didn't want to find.&lt;/p&gt;

&lt;p&gt;Never stop coding!&lt;/p&gt;

</description>
      <category>git</category>
      <category>beginners</category>
      <category>tutorial</category>
      <category>devops</category>
    </item>
    <item>
      <title>Your First Code Conversation with AI</title>
      <dc:creator>Javi Palacios</dc:creator>
      <pubDate>Tue, 11 Aug 2026 06:42:13 +0000</pubDate>
      <link>https://dev.to/fj_palacios/your-first-code-conversation-with-ai-4k2k</link>
      <guid>https://dev.to/fj_palacios/your-first-code-conversation-with-ai-4k2k</guid>
      <description>&lt;p&gt;The first real coding message you send to an AI is usually the wrong one. Too vague, missing half the context, not entirely sure what you're even asking for. You type something, look at it, delete it, rephrase, look at it again — and somehow the blank cursor feels more intimidating than any compiler error you've ever seen.&lt;/p&gt;

&lt;p&gt;Here's the thing: send it anyway. The first message doesn't need to be good. Getting the dialogue started is the work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dialogue, not search
&lt;/h2&gt;

&lt;p&gt;Using AI for code is not like searching Stack Overflow or querying a documentation site. With search, you get one shot — a vague query returns vague results and you start over. There's no back-and-forth, no way to clarify, no memory of what you tried before.&lt;/p&gt;

&lt;p&gt;With AI it works differently: it's a &lt;strong&gt;conversation&lt;/strong&gt;. You can start imprecise and sharpen as you go. You can ask why it chose a particular approach. You can say "this doesn't work" and continue from there without repeating all the context. The model carries everything said in the current session — you're always building on what came before.&lt;/p&gt;

&lt;p&gt;That's the mental shift. Not "I need to formulate the perfect question." It's "I'll build toward the right answer together with it."&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting up your first session
&lt;/h2&gt;

&lt;p&gt;If you're using Claude, open &lt;a href="https://claude.ai" rel="noopener noreferrer"&gt;claude.ai&lt;/a&gt;. The free tier covers everything in these first modules — no API key, no terminal, no configuration required. A text field and a send button.&lt;/p&gt;

&lt;p&gt;Do you need a well-crafted prompt before starting? No — Module 2 is entirely about that. For now, the only requirement is an open chat window and a real problem.&lt;/p&gt;

&lt;p&gt;One thing to understand from the start: &lt;strong&gt;each conversation is independent&lt;/strong&gt;. The model has no memory of previous sessions. Everything we covered in earlier tutorials about hallucinations and context blindness applies here: it only knows what's in &lt;em&gt;this&lt;/em&gt; conversation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Experiment 1: ask it to explain something
&lt;/h2&gt;

&lt;p&gt;For your very first session, work in familiar territory. Don't start by asking it to build something from scratch — start by asking it to explain code that already exists.&lt;/p&gt;

&lt;p&gt;Take this Python snippet:&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="n"&gt;numbers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;numbers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;)[:&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And write something like:&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="n"&gt;Explain&lt;/span&gt; &lt;span class="n"&gt;what&lt;/span&gt; &lt;span class="n"&gt;this&lt;/span&gt; &lt;span class="n"&gt;Python&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt; &lt;span class="n"&gt;does&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="n"&gt;by&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;

&lt;span class="n"&gt;numbers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;numbers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;)[:&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The response comes back in a few seconds — formatted, technically precise, with clear explanations. Your first reaction might be: "that's it?" Yes. Now the important part starts.&lt;/p&gt;

&lt;p&gt;Does the explanation make sense? Does it describe the behavior you'd actually see if you ran the code? Run it:&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="n"&gt;numbers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;numbers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;)[:&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# [9, 6, 5]
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Did the explanation predict that output? If yes — good. If not — that's useful information about the limits of that particular response. Not a failure, just data.&lt;/p&gt;

&lt;p&gt;This isn't a test you're running on the AI. It's the process: receive, understand, verify. Always in that order.&lt;/p&gt;

&lt;h2&gt;
  
  
  Experiment 2: ask it to fix something
&lt;/h2&gt;

&lt;p&gt;The second experiment is closer to everyday use. You have broken code. You hand it to the AI.&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;calculate_average&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;numbers&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;num&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;numbers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;num&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;numbers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;calculate_average&lt;/span&gt;&lt;span class="p"&gt;([]))&lt;/span&gt;  &lt;span class="c1"&gt;# ZeroDivisionError
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The bug is obvious if you have experience. But if you're new to Python, or you're in the middle of something larger and don't have the bandwidth for this right now, the AI can solve it in seconds:&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="n"&gt;This&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt; &lt;span class="n"&gt;throws&lt;/span&gt; &lt;span class="nb"&gt;ZeroDivisionError&lt;/span&gt; &lt;span class="n"&gt;when&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="n"&gt;empty&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt; &lt;span class="n"&gt;How&lt;/span&gt; &lt;span class="n"&gt;do&lt;/span&gt; &lt;span class="n"&gt;I&lt;/span&gt; &lt;span class="n"&gt;fix&lt;/span&gt; &lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="err"&gt;?&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;calculate_average&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;numbers&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;num&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;numbers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;num&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;numbers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The response will likely propose something like:&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;calculate_average&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;numbers&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;numbers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="c1"&gt;# Handle empty list
&lt;/span&gt;        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;  &lt;span class="c1"&gt;# or None, or raise ValueError — depends on your use case
&lt;/span&gt;    &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;num&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;numbers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;num&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;numbers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice: the AI will probably give you options — &lt;code&gt;return 0&lt;/code&gt;, &lt;code&gt;return None&lt;/code&gt;, &lt;code&gt;raise ValueError&lt;/code&gt;. That's not hedging, it's accurate. It doesn't know which behavior fits your project. That decision belongs to you.&lt;/p&gt;

&lt;p&gt;There's a specific feeling that comes with pasting AI-generated code into your editor — not quite confidence, not quite skepticism. Somewhere in between. That feeling is correct. It means you're thinking, not just copying. Don't override it. Read the fix, understand why it works, pick the option that matches your actual requirements.&lt;/p&gt;

&lt;h2&gt;
  
  
  Experiment 3: ask it to generate something
&lt;/h2&gt;

&lt;p&gt;The third experiment is the one that impresses you first — and the one that requires the most care. Give the AI a specification and watch it write code from scratch.&lt;/p&gt;

&lt;p&gt;Start with something small and verifiable:&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="n"&gt;Write&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="n"&gt;Python&lt;/span&gt; &lt;span class="n"&gt;function&lt;/span&gt; &lt;span class="n"&gt;that&lt;/span&gt; &lt;span class="n"&gt;takes&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt; &lt;span class="n"&gt;of&lt;/span&gt; &lt;span class="n"&gt;strings&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;returns&lt;/span&gt; &lt;span class="n"&gt;only&lt;/span&gt; &lt;span class="n"&gt;those&lt;/span&gt;
&lt;span class="n"&gt;longer&lt;/span&gt; &lt;span class="n"&gt;than&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="n"&gt;characters&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;sorted&lt;/span&gt; &lt;span class="n"&gt;alphabetically&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt; &lt;span class="n"&gt;Include&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="n"&gt;usage&lt;/span&gt; &lt;span class="n"&gt;example&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The response will be something like:&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;filter_and_sort_strings&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;strings&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# Filter strings longer than 5 characters, then sort alphabetically
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;strings&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="c1"&gt;# Example usage
&lt;/span&gt;&lt;span class="n"&gt;words&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cat&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;banana&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;apple&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;elephant&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fig&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mango&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;filter_and_sort_strings&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;words&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# ['banana', 'elephant', 'mango']
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run the example. Verify the output matches what you expected. Then test with your own cases:&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="c1"&gt;# What about empty strings?
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;filter_and_sort_strings&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;a&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hello world&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt;  &lt;span class="c1"&gt;# ['hello world']
&lt;/span&gt;
&lt;span class="c1"&gt;# What about exactly 5 characters?
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;filter_and_sort_strings&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;exact&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;longer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt;  &lt;span class="c1"&gt;# ['longer'] — "exact" is 5 chars, not &amp;gt; 5
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Does the behavior on edge cases match your use case? The code is correct in the abstract. If you wanted "5 or more characters" instead of "more than 5", that difference is yours to catch — the AI implemented exactly what you asked for, not necessarily what you meant.&lt;/p&gt;

&lt;p&gt;That gap — between correct generation and &lt;em&gt;appropriate&lt;/em&gt; generation — is the one that bites you in production. The first is the AI's job. The second is yours.&lt;/p&gt;

&lt;h2&gt;
  
  
  The evaluation loop
&lt;/h2&gt;

&lt;p&gt;After every response — explanation, fix, or generated code — three questions before you use it:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does it run?&lt;/strong&gt; Execute it. An immediate &lt;code&gt;AttributeError&lt;/code&gt; or &lt;code&gt;SyntaxError&lt;/code&gt; is data: something in the response isn't right. Unexpected output is also data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do I understand it?&lt;/strong&gt; If you can't explain what each line does, don't merge it. Not because the AI is unreliable — because code you don't understand is code you can't debug when things go wrong. And things always go wrong eventually.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does it fit my context?&lt;/strong&gt; AI-generated code works in the abstract. Your project has conventions, dependencies, constraints. Does the solution respect them? Or does it assume something that isn't true in your system?&lt;/p&gt;

&lt;p&gt;These questions are fast for simple cases. For a &lt;code&gt;sorted()&lt;/code&gt; with a custom key, a glance is enough. For authentication code, data validation, or business logic — these become a real review, not a quick skim.&lt;/p&gt;

&lt;h2&gt;
  
  
  The challenge: build a calculator
&lt;/h2&gt;

&lt;p&gt;You've seen the three dynamics — explain, fix, generate. Now combine them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;💡 Challenge:&lt;/strong&gt; Use AI to build a simple Python calculator. Requirements:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Accept two numbers and an operation (&lt;code&gt;+&lt;/code&gt;, &lt;code&gt;-&lt;/code&gt;, &lt;code&gt;*&lt;/code&gt;, &lt;code&gt;/&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Return the result&lt;/li&gt;
&lt;li&gt;Handle division by zero gracefully&lt;/li&gt;
&lt;li&gt;Include at least one usage example per operation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One rule: verify each piece of code you receive before asking for the next one. Don't paste the entire solution in a single shot.&lt;/p&gt;

&lt;p&gt;When you're done, you'll have completed your first end-to-end AI-assisted coding session.&lt;/p&gt;




&lt;p&gt;You've used the AI to explain, fix, and generate code. The cycle is three steps: receive, understand, verify. And you've seen directly that the AI doesn't make context decisions for you — those belong to you.&lt;/p&gt;

&lt;p&gt;In the next tutorial, we start &lt;strong&gt;Module 2: Effective Prompting for Developers&lt;/strong&gt;. First topic: the RCTF framework — Role, Context, Task, Format — the concrete difference between a prompt that gets what you need and one that returns something plausible but not useful.&lt;/p&gt;

&lt;p&gt;Never stop coding!&lt;/p&gt;

</description>
      <category>ai</category>
      <category>beginners</category>
      <category>tutorial</category>
      <category>programming</category>
    </item>
    <item>
      <title>The AI Model Landscape in 2026: who's who and where to start</title>
      <dc:creator>Javi Palacios</dc:creator>
      <pubDate>Fri, 07 Aug 2026 12:41:02 +0000</pubDate>
      <link>https://dev.to/fj_palacios/the-ai-model-landscape-in-2026-whos-who-and-where-to-start-32ip</link>
      <guid>https://dev.to/fj_palacios/the-ai-model-landscape-in-2026-whos-who-and-where-to-start-32ip</guid>
      <description>&lt;p&gt;Let me be upfront about something: this tutorial will age badly. Not the concepts — those hold. But the specific names, the price points, the rankings — the AI model landscape moves fast enough that any comparison table has an expiration date. What's the best model for code today might be second place next quarter. The one that seems expensive now might be the obvious choice by the time you're reading this.&lt;/p&gt;

&lt;p&gt;That said: the &lt;strong&gt;mental model&lt;/strong&gt; for navigating this landscape doesn't expire. What questions to ask when choosing a model, how API access differs from a subscription, what "context window" actually means on a regular Tuesday — that's stable. With the right map, you can update yourself when things change. And they will.&lt;/p&gt;

&lt;h2&gt;
  
  
  The frontier models: the big three
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Frontier models&lt;/strong&gt; are the most capable models at any given moment. In 2026, the competition comes down to three:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Claude Sonnet 4.6 (Anthropic)&lt;/strong&gt; is the model you'll be using throughout this course, and the current benchmark for coding tasks. Together with Opus 4.7, it ships with a &lt;strong&gt;1M token context window at standard pricing&lt;/strong&gt; — no special headers, no premium plan required. It stands out for sustained reasoning, precise technical writing, and following complex multi-step instructions. Sonnet 4.6 is the speed-quality balance; Opus 4.7 is more powerful but slower and more expensive.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GPT-5.4 (OpenAI)&lt;/strong&gt; — released March 2026 — is the first general-purpose model with &lt;strong&gt;native computer use&lt;/strong&gt;: it can operate desktop interfaces and execute complex workflows across applications. It reaches 1M tokens of context, incorporates the coding capabilities of GPT-5.3-Codex, and comes in multiple variants — Thinking, Pro, mini (free tier), and nano (API-only) — making it the most accessible of the three. Recently, GPT-5.5 has been released, with improvements in speed and reasoning.&lt;/p&gt;

&lt;p&gt;OpenAI also maintains the &lt;strong&gt;Codex&lt;/strong&gt; family as a separate line: GPT-5.3-Codex is optimized for complex agentic software engineering and leads benchmarks like SWE-Bench Pro; GPT-5.3-Codex-Spark is its ultra-fast variant, designed for real-time response. They're not always the best choice — for general reasoning or mixed tasks, GPT-5.4 covers more ground — but if your use case is intensive agentic engineering, they're worth knowing about.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Gemini 3.1 Pro (Google)&lt;/strong&gt; — released February 2026 — has a 1M token context window and strong performance on complex reasoning benchmarks: 77.1% on ARC-AGI-2, which measures the ability to solve entirely new logical patterns. Its natural advantage remains Google ecosystem integration — Workspace, Cloud, Android, NotebookLM. For projects already living in Google Cloud, it's hard to ignore.&lt;/p&gt;

&lt;p&gt;The "which is best?" question has an easy answer and an honest one. Easy: Claude. Honest: it depends on the task, changes every few months, and benchmarks published with great fanfare on a Friday tend to be outdated before anyone seriously cites them the following Monday. The real practical difference between all three in day-to-day use is smaller than those leaderboards suggest.&lt;/p&gt;

&lt;p&gt;There's also something counterintuitive worth naming: for everyday work, the &lt;strong&gt;tool&lt;/strong&gt; wrapping the model matters more than the model itself. Copilot with GPT-4o integrated directly into your editor can be more productive than Claude with no integration, even if Claude outperforms it on whatever benchmark you prefer. More on that in the next section.&lt;/p&gt;

&lt;h2&gt;
  
  
  Specialized coding tools
&lt;/h2&gt;

&lt;p&gt;Beyond the frontier models, there are tools built specifically for development workflows. The key distinction: these aren't models — they're &lt;strong&gt;interfaces and assistants&lt;/strong&gt; that use frontier models under the hood.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub Copilot&lt;/strong&gt; — specifically its &lt;strong&gt;Pro+&lt;/strong&gt; plan — is the option with the widest model roster inside a single environment. You can switch between Claude Sonnet 4.6, GPT-5.3-Codex, Gemini 3 Pro, and others directly from the chat interface, without changing tools. The VS Code and JetBrains integration is the most mature on the market, with an autonomous agent that can edit files, run terminal commands, and iterate on errors without manual intervention.&lt;/p&gt;

&lt;p&gt;Its weak point is context: the native limit is 128K tokens, well below the 1M of the frontier models. Workspace mode partially compensates with RAG indexing of your codebase, but that's not the same as real coherence across a long context window. If you need long sessions with a lot of code in flight, Cursor or OpenCode have the edge. If you value model variety and deep editor integration, Copilot Pro+ is hard to beat.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cursor&lt;/strong&gt; is a full editor (a VS Code fork) with AI woven into the editing flow. You talk to it about your code, make changes across multiple files from a single instruction, and review diffs before applying. It's more assertive than Copilot — it doesn't just complete, it collaborates. If the workflow clicks for you, going back feels like a downgrade.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;OpenCode&lt;/strong&gt; is the terminal agent you'll use from Module 3 onward in this course. Unlike Copilot or Cursor, it has no monthly subscription of its own — it connects to whichever providers you choose, either by logging into your existing account or by supplying an API key. OpenCode orchestrates; the model comes from whoever you already have a contract with.&lt;/p&gt;

&lt;p&gt;To get started without spending anything, OpenCode includes 8 free models through its Zen service (CLI-only): mainly Chinese models — MiniMax, GLM, Kimi — that let you explore the tool without a credit card. They have real limitations in speed, reasoning, and context; they're a starting point, not a substitute for frontier models.&lt;/p&gt;

&lt;p&gt;On the other hand, there’s OpenCode Go, which, for $5 in the first month and $10 a month thereafter, gives you access to a curated selection of high-quality Chinese models — the latest versions of those mentioned above and others — with fairly generous usage-based limits. It’s a middle-of-the-road option for those who want more power and longer usage time than the free OpenCode Zen models offer, but don’t want to commit to the more expensive plans for the Frontier models.&lt;/p&gt;

&lt;p&gt;When you want more power, you have several paths:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Log in with your provider account&lt;/strong&gt;: if you already have a subscription with OpenAI, Google, or others, you can use it directly from OpenCode — the same way you would from their web interface or official app.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OpenCode Zen&lt;/strong&gt; (pay-as-you-go): token access with no markup — you pay only the card transaction cost on top of the provider's price.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your own API key&lt;/strong&gt;: connect directly to whichever provider you prefer.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One important exception: &lt;strong&gt;Anthropic&lt;/strong&gt;. It has explicitly banned the use of its monthly subscription plans (Claude Pro) with third-party tools like OpenCode — API access only. This isn't an OpenCode limitation — it's a unilateral Anthropic policy decision that could be repeated by other providers; worth keeping in mind from the start.&lt;/p&gt;

&lt;p&gt;Everything I’ve told you in these last two sections — the best models, prices, etc. — will generally still hold true, but the specific model names and their prices may change. The important thing is to understand the big picture: what each type of model does, the difference between API and subscription, and how to choose the tool that best suits your workflow.&lt;/p&gt;

&lt;h3&gt;
  
  
  The CLI ecosystem and editor plugins
&lt;/h3&gt;

&lt;p&gt;OpenCode isn't the only terminal agent. The major AI companies have their own CLI tools that work very similarly — same philosophy, same command-line access:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Claude Code&lt;/strong&gt; (Anthropic): Anthropic's official CLI, with a VS Code plugin already available and especially well integrated.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Codex CLI&lt;/strong&gt; (OpenAI): OpenAI's terminal agent, focused on intensive software engineering tasks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Copilot CLI&lt;/strong&gt; (GitHub): GitHub Copilot's terminal extension.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This course uses OpenCode as the reference to avoid multiplying examples across every tool, but the mental model you'll build applies equally to all of them.&lt;/p&gt;

&lt;p&gt;For those who'd rather not open a separate terminal window: every code editor has an &lt;strong&gt;embedded terminal&lt;/strong&gt;. If a specific plugin doesn't exist yet for your preferred tool — or isn't as well-integrated as Claude Code's VS Code plugin — running the agent from that embedded terminal works without issues.&lt;/p&gt;

&lt;p&gt;Try answering "which AI tool do you use for coding?" without a thirty-second preamble. It's harder than it sounds — not because you don't know, but because the boundary between these tools shifts faster than anyone can track, and the underlying model a given tool uses quietly changes every few months anyway. The real question isn't "which is better" but "at which point in your workflow do you want AI to show up."&lt;/p&gt;

&lt;h2&gt;
  
  
  API or subscription: the difference that matters
&lt;/h2&gt;

&lt;p&gt;There are two ways to access these models, and the confusion between them is more common than you'd expect — partly because providers use the same product names to mean different things depending on context. If this has never been fully clear to you, that's not a gap in your knowledge; it's a gap in how the industry communicates.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A subscription&lt;/strong&gt; (Claude Pro, ChatGPT Plus, Gemini Advanced) is flat-rate access: fixed monthly fee, and you can use the model from the provider's web interface or app, from their own tools (CLI, desktop, editor plugins), and in most cases from third-party tools like OpenCode by logging into your account. It has usage limits — tokens per day, requests per week, depending on the provider — and when you hit them, access is cut off until the counter resets. The upside is predictability: you know exactly what you'll pay this month.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The API&lt;/strong&gt; is programmatic, pay-per-use access: you get a key and call the model from your code or from whatever integration you need to build. You pay for the tokens you send and the tokens you receive, not for time. Rate limits still exist — per minute or per hour, depending on provider and tier — but there's no fixed monthly ceiling: consume more, pay more. The cost is variable, and depending on usage, it can easily exceed what a subscription would have cost.&lt;/p&gt;

&lt;p&gt;The real distinction, then, isn't "basic vs. advanced" or "for the web vs. for tools" — it's predictable fixed cost versus variable, uncapped cost. The API is necessary when you want to build your own application with AI capabilities, or when a provider doesn't allow using your subscription with third-party tools (Anthropic's case, as we saw). For everything else, a subscription gets you far.&lt;/p&gt;

&lt;p&gt;Do you need the paid API to follow this course? Spoiler: you don't. The early modules use the web interface, covered by the free or basic subscription tier. The API comes in when we reach OpenCode — and at that point, typical learning usage costs less than you'd expect.&lt;/p&gt;

&lt;h2&gt;
  
  
  The context window: beyond the number
&lt;/h2&gt;

&lt;p&gt;Every model advertises its context window in tokens. All three frontier models — Claude 4.6, GPT-5.4, and Gemini 3.1 Pro — now reach one million. The number sounds impressive.&lt;/p&gt;

&lt;p&gt;Reality check: the context window is how much information the model can "see" in a single conversation — your message, the conversation history, and any files or code you've attached. Everything together.&lt;/p&gt;

&lt;p&gt;200k tokens sounds like a lot. In practice, that's roughly 150,000 words or about 600KB of code. Enough to paste an entire medium-sized codebase. For answering a quick question about one function, 2,000 tokens is plenty.&lt;/p&gt;

&lt;p&gt;What actually matters isn't the maximum number — it's two things. First, whether the model &lt;strong&gt;maintains coherence&lt;/strong&gt; across a long context (not all models do this equally well). Second, how &lt;strong&gt;cost scales&lt;/strong&gt; as the context grows, because in the API you pay for input tokens too. A long conversation with a large codebase attached can burn through budget faster than expected.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rate limits and how they affect real work
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Rate limits&lt;/strong&gt; are the caps providers put on how many requests or tokens you can send per minute, hour, or day. They vary by plan and provider.&lt;/p&gt;

&lt;p&gt;For learning purposes, you won't notice them. Rate limits become a real problem when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You're running the API in production with multiple simultaneous users&lt;/li&gt;
&lt;li&gt;You have an automated process making many calls in sequence&lt;/li&gt;
&lt;li&gt;You're on the free tier during a heavy working session&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The signal that you've hit a rate limit is usually a &lt;code&gt;429 Too Many Requests&lt;/code&gt; error. The fix: wait a few minutes or upgrade your plan. It's not a code error — it's a usage cap.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this actually costs
&lt;/h2&gt;

&lt;p&gt;I'll save you the deliberation: for learning, cost is essentially a non-issue. Most models have free tiers that comfortably cover the first few weeks of use. Claude.ai has a free tier. ChatGPT has a free tier. Basic subscriptions are around $20/month.&lt;/p&gt;

&lt;p&gt;The API is where cost becomes variable — and that variability can surprise you the first time you see the bill after an afternoon debugging session that went longer than planned. Pricing is measured in &lt;strong&gt;dollars per million tokens&lt;/strong&gt;. In 2026, Claude Sonnet is around $3 per million input tokens and $15 per million output tokens. GPT-5.4 is in a similar range.&lt;/p&gt;

&lt;p&gt;What does that mean practically? A typical working session with OpenCode — a few hours of back-and-forth with code attached and project context — runs between $0.50 and $3. An intensive session with a large codebase and many iterations might reach $10. For daily learning use, the monthly total usually lands between $5 and $20.&lt;/p&gt;

&lt;p&gt;Practical advice: set a spending limit on your API account from day one. Every provider lets you configure this. Not because you'll accidentally spend a fortune, but because knowing there's a ceiling is worth the 30 seconds it takes to set it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to actually start
&lt;/h2&gt;

&lt;p&gt;With all of this on the table, the real question is: what do I do right now?&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Situation&lt;/th&gt;
&lt;th&gt;Recommendation&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Just discovered this&lt;/td&gt;
&lt;td&gt;Claude.ai free — no setup, no cost, straight to the point&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Want to try multiple models without leaving the editor&lt;/td&gt;
&lt;td&gt;GitHub Copilot Pro+ (widest model roster)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Want the most integrated editor experience&lt;/td&gt;
&lt;td&gt;Cursor (free trial available)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Need long sessions with genuinely large context&lt;/td&gt;
&lt;td&gt;OpenCode or Cursor (real 1M tokens, not RAG)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Following this course through Module 3&lt;/td&gt;
&lt;td&gt;Claude.ai until then, then OpenCode&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ready to start with the API&lt;/td&gt;
&lt;td&gt;Claude API with a spending limit configured&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;You don't have to pick one forever. Most developers who've been working with these tools for a while use two or three depending on context — the web interface for quick exploration, an editor tool for autocomplete, and a terminal agent for tasks that need file access and command execution.&lt;/p&gt;




&lt;p&gt;You now have the map. You know who the players are, how models differ from tools built on top of them, when the API makes sense, and how to think about cost without letting it become a blocker. In the next tutorial we get into &lt;strong&gt;real conversations with AI&lt;/strong&gt;: how to structure a code prompt, what context to include, and how to evaluate the response before you commit it to your project.&lt;/p&gt;

&lt;p&gt;Never stop coding!&lt;/p&gt;

</description>
      <category>ai</category>
      <category>beginners</category>
      <category>tutorial</category>
      <category>programming</category>
    </item>
    <item>
      <title>for loops and range() in Python: iterate without the busywork</title>
      <dc:creator>Javi Palacios</dc:creator>
      <pubDate>Thu, 06 Aug 2026 13:34:23 +0000</pubDate>
      <link>https://dev.to/fj_palacios/for-loops-and-range-in-python-iterate-without-the-busywork-4fi9</link>
      <guid>https://dev.to/fj_palacios/for-loops-and-range-in-python-iterate-without-the-busywork-4fi9</guid>
      <description>&lt;p&gt;You already know how to count from 1 to 10 with &lt;code&gt;while&lt;/code&gt;. A counter, a condition, an increment — four lines of code where three of them exist only to serve the fourth. That works. But Python has a better tool for situations like this:&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="c1"&gt;# while: you manage the counter yourself
&lt;/span&gt;&lt;span class="n"&gt;counter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;counter&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;counter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;counter&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;

&lt;span class="c1"&gt;# for: Python manages it for you
&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same result. Half the ceremony. The difference isn't just aesthetic — it's about intent. The &lt;code&gt;for&lt;/code&gt; loop says "I know exactly what I want to iterate over." The &lt;code&gt;while&lt;/code&gt; says "keep going until this condition is false." When you know the sequence upfront, &lt;code&gt;for&lt;/code&gt; is the right tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is a for loop?
&lt;/h2&gt;

&lt;p&gt;A &lt;code&gt;for&lt;/code&gt; loop iterates over a &lt;strong&gt;sequence&lt;/strong&gt; of elements — one at a time, in order — and runs a block of code for each one. When it runs out of elements, it stops on its own.&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="n"&gt;fruits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;apple&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pear&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;grape&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;fruit&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;fruits&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fruit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# Print each fruit
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;apple
pear
grape
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The structure is &lt;code&gt;for &amp;lt;variable&amp;gt; in &amp;lt;sequence&amp;gt;:&lt;/code&gt;. On each iteration, the variable (&lt;code&gt;fruit&lt;/code&gt;) takes the value of the next element. No indexing, no incrementing — the loop handles it.&lt;/p&gt;

&lt;p&gt;One thing that catches beginners off guard: the loop variable doesn't disappear when the loop ends. After the &lt;code&gt;for&lt;/code&gt; finishes, it's still in scope, holding the value from the last iteration.&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="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;fruit&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;fruits&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fruit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fruit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# "grape" — still accessible after the loop
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's not a bug — it's just how Python scoping works for loop variables. File this away; you'll appreciate knowing it the first time you need to debug something after a loop.&lt;/p&gt;

&lt;h2&gt;
  
  
  range(): generating number sequences
&lt;/h2&gt;

&lt;p&gt;Iterating over an existing list is useful, but often you just want to repeat something a set number of times. That's what &lt;code&gt;range()&lt;/code&gt; is for — it generates a sequence of integers on demand.&lt;/p&gt;

&lt;p&gt;It comes in three forms:&lt;/p&gt;

&lt;h3&gt;
  
  
  range(stop)
&lt;/h3&gt;

&lt;p&gt;The simplest: generates numbers from &lt;code&gt;0&lt;/code&gt; up to (but not including) &lt;code&gt;stop&lt;/code&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="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;0
1
2
3
4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice: &lt;code&gt;range(5)&lt;/code&gt; gives you five numbers starting at &lt;code&gt;0&lt;/code&gt;. The stop value is &lt;strong&gt;not included&lt;/strong&gt; — this is the same "half-open interval" convention you'll see everywhere in Python, from slicing to indexing.&lt;/p&gt;

&lt;h3&gt;
  
  
  range(start, stop)
&lt;/h3&gt;

&lt;p&gt;When you want to start somewhere other than zero:&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="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# 1, 2, 3, 4, 5
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1
2
3
4
5
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Clean and readable — no &lt;code&gt;+ 1&lt;/code&gt; hacks.&lt;/p&gt;

&lt;h3&gt;
  
  
  range(start, stop, step)
&lt;/h3&gt;

&lt;p&gt;The third argument controls the step — how much the sequence advances on each iteration.&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="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# Even numbers from 0 to 10
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;0
2
4
6
8
10
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A negative step lets you count backwards:&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="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# Countdown from 10 to 1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;10
9
8
7
6
5
4
3
2
1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Don't stress about memorizing all three forms right now. You'll use &lt;code&gt;range(n)&lt;/code&gt; the vast majority of the time — "repeat this n times" covers most everyday cases. The rest you'll pick up as you go.&lt;/p&gt;

&lt;h2&gt;
  
  
  Iterating over other sequences
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;for&lt;/code&gt; loops aren't limited to lists and ranges. They work with anything &lt;strong&gt;iterable&lt;/strong&gt; — which, for now, you can read as "anything that has elements to loop through."&lt;/p&gt;

&lt;p&gt;Strings are iterable — each character is its own element:&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="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;char&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Python&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;char&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;P
y
t
h
o
n
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You'll get a full introduction to lists in the next tutorial, but the pattern is exactly the same as you saw with the fruits example. The &lt;code&gt;for&lt;/code&gt; loop is generic — it doesn't care whether you're going through characters, numbers, or anything else. The mechanism is always the same.&lt;/p&gt;

&lt;h2&gt;
  
  
  break: stop early
&lt;/h2&gt;

&lt;p&gt;Just like with &lt;code&gt;while&lt;/code&gt;, you can cut a &lt;code&gt;for&lt;/code&gt; loop short with &lt;code&gt;break&lt;/code&gt;. The moment Python hits a &lt;code&gt;break&lt;/code&gt;, it exits the loop immediately — no remaining elements are processed.&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="n"&gt;numbers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;number&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;numbers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;number&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Found 9, stopping.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;break&lt;/span&gt;   &lt;span class="c1"&gt;# No need to keep looking
&lt;/span&gt;    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;number&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;3
7
2
Found 9, stopping.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once &lt;code&gt;break&lt;/code&gt; fires, &lt;code&gt;4&lt;/code&gt;, &lt;code&gt;6&lt;/code&gt;, and &lt;code&gt;1&lt;/code&gt; never get touched. If what you're looking for appears early in a long list, &lt;code&gt;break&lt;/code&gt; saves you from processing the entire thing.&lt;/p&gt;

&lt;p&gt;Here's where it gets interesting: Python has an &lt;code&gt;else&lt;/code&gt; clause for &lt;code&gt;for&lt;/code&gt; loops. The &lt;code&gt;else&lt;/code&gt; block runs only if the loop completed &lt;strong&gt;without&lt;/strong&gt; hitting a &lt;code&gt;break&lt;/code&gt;. If &lt;code&gt;break&lt;/code&gt; fires, the &lt;code&gt;else&lt;/code&gt; is skipped entirely:&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="n"&gt;emails&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user@example.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;admin@site.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;support@company.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;target&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;another@email.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;emails&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Email found.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;break&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Email not found in the list.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# Only runs if no break occurred
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Email not found in the list.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you've used other languages and this &lt;code&gt;for-else&lt;/code&gt; pattern feels strange — you're not alone. It surprises most people the first time. But once it clicks, it's genuinely useful for "search and report" scenarios where you want different behavior depending on whether the search succeeded.&lt;/p&gt;

&lt;h2&gt;
  
  
  Nested loops: a for inside a for
&lt;/h2&gt;

&lt;p&gt;You can put a &lt;code&gt;for&lt;/code&gt; inside another &lt;code&gt;for&lt;/code&gt;. For every iteration of the outer loop, the inner loop runs completely through all of its iterations before the outer loop moves on.&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="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;col&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&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="s"&gt;(&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;row&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;col&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&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;end&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# Print on same line
&lt;/span&gt;    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  &lt;span class="c1"&gt;# New line after each row
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;(1,1) (1,2) (1,3)
(2,1) (2,2) (2,3)
(3,1) (3,2) (3,3)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The outer loop handles rows; the inner loop handles columns. A multiplication table is the classic example:&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="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;j&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;result&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;3&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;end&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# Right-align in 3 chars
&lt;/span&gt;    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  1  2  3  4  5
  2  4  6  8 10
  3  6  9 12 15
  4  8 12 16 20
  5 10 15 20 25
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nested loops work well for two-dimensional structures: tables, grids, coordinate pairs. One thing to keep in mind: the operations &lt;strong&gt;multiply&lt;/strong&gt;. A loop of 100 inside a loop of 100 is 10,000 total operations. Add a third loop of 100 and you're at 1,000,000. Two levels of nesting is fine; three levels is usually a sign to rethink the approach.&lt;/p&gt;

&lt;h2&gt;
  
  
  for vs while: the definitive breakdown
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Situation&lt;/th&gt;
&lt;th&gt;Use&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;You know how many times to repeat&lt;/td&gt;
&lt;td&gt;&lt;code&gt;for&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;You have a sequence to iterate over&lt;/td&gt;
&lt;td&gt;&lt;code&gt;for&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reading a file from start to finish&lt;/td&gt;
&lt;td&gt;&lt;code&gt;for&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Iterating over a list, string, or range&lt;/td&gt;
&lt;td&gt;&lt;code&gt;for&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The stopping condition depends on external input&lt;/td&gt;
&lt;td&gt;&lt;code&gt;while&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Waiting for valid user input&lt;/td&gt;
&lt;td&gt;&lt;code&gt;while&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A game that runs until the player loses&lt;/td&gt;
&lt;td&gt;&lt;code&gt;while&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Retry loops with unpredictable timing&lt;/td&gt;
&lt;td&gt;&lt;code&gt;while&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;In practice, you'll reach for &lt;code&gt;for&lt;/code&gt; far more often than &lt;code&gt;while&lt;/code&gt;. Most iteration in Python has a concrete sequence to go through, and &lt;code&gt;for&lt;/code&gt; handles it more clearly and directly than managing the loop yourself.&lt;/p&gt;




&lt;p&gt;With &lt;code&gt;for&lt;/code&gt; and &lt;code&gt;range()&lt;/code&gt; in your toolkit, you can iterate over any sequence without tracking a counter. The natural next step is having something interesting to iterate over: in the next tutorial you'll learn about &lt;strong&gt;lists&lt;/strong&gt; — Python's most versatile data structure — including how to create them, access elements by index, and combine them with everything you've learned so far.&lt;/p&gt;

&lt;p&gt;Never stop coding!&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;💡 Challenge&lt;/strong&gt;: Write a program that generates and prints the multiplication tables from 1 to 10, formatted so each result takes the same amount of space (try &lt;code&gt;f"{result:4}"&lt;/code&gt; for alignment). Bonus: ask the user to enter a number between 1 and 10 and show only that table — with the option to request another one until they decide to quit.&lt;/p&gt;

</description>
      <category>python</category>
      <category>beginners</category>
      <category>tutorial</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
