<?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: JAGADEESH .M</title>
    <description>The latest articles on DEV Community by JAGADEESH .M (@jagadeesh_m_8870).</description>
    <link>https://dev.to/jagadeesh_m_8870</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%2F4229600%2F2e7db760-c9a1-4005-a918-7cb0a622c0ad.jpg</url>
      <title>DEV Community: JAGADEESH .M</title>
      <link>https://dev.to/jagadeesh_m_8870</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/jagadeesh_m_8870"/>
    <language>en</language>
    <item>
      <title>Why PDF Bounding Box Coordinates Break in Python (and How to Fix Them)</title>
      <dc:creator>JAGADEESH .M</dc:creator>
      <pubDate>Fri, 06 Nov 2026 12:24:36 +0000</pubDate>
      <link>https://dev.to/jagadeesh_m_8870/why-pdf-bounding-box-coordinates-break-in-python-and-how-to-fix-them-4bbp</link>
      <guid>https://dev.to/jagadeesh_m_8870/why-pdf-bounding-box-coordinates-break-in-python-and-how-to-fix-them-4bbp</guid>
      <description>&lt;p&gt;If you have ever built an automated document processing pipeline—such as extracting data from tax returns, invoices, insurance claims, or contracts—you have almost certainly hit &lt;strong&gt;the coordinate drift nightmare&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;You carefully define a bounding box for an input field:&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;bbox&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;100.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;150.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;350.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;180.0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;  &lt;span class="c1"&gt;# [x0, y0, x1, y1]
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You run your extraction code, and:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Half the text is cut off.&lt;/li&gt;
&lt;li&gt;Words from the row above or below bleed into your output.&lt;/li&gt;
&lt;li&gt;On rotated pages or cropped scans, your bounding boxes end up completely misplaced.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In this article, we'll examine why PDF coordinate extraction is so error-prone in Python, the mathematical root causes behind coordinate drift, and how to achieve 100% deterministic spatial extraction.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Root Causes: Why PDF Coordinates Drift
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. The Inverted Origin Trap (Bottom-Left vs. Top-Left)
&lt;/h3&gt;

&lt;p&gt;According to the official Adobe PDF specification (ISO 32000), the default PDF user coordinate system places the coordinate origin &lt;code&gt;(0, 0)&lt;/code&gt; at the &lt;strong&gt;BOTTOM-LEFT&lt;/strong&gt; corner of the physical page:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;$X$ increases from left to right.&lt;/li&gt;
&lt;li&gt;$Y$ increases from &lt;strong&gt;bottom to top&lt;/strong&gt; ($Y_{\text{up}}$).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;However, computer screens, GUI canvases, and modern computer vision tools (OpenCV, PIL, browser viewports) place &lt;code&gt;(0, 0)&lt;/code&gt; at the &lt;strong&gt;TOP-LEFT&lt;/strong&gt; corner:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;$X$ increases from left to right.&lt;/li&gt;
&lt;li&gt;$Y$ increases from &lt;strong&gt;top to bottom&lt;/strong&gt; ($Y_{\text{down}}$).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;$$\text{Screen } Y = \text{Page Height} - \text{PDF } Y$$&lt;/p&gt;

&lt;p&gt;If an extraction library does not rigorously track page height and media boundaries, a coordinate transformation will silently invert or shift your text bounding boxes.&lt;/p&gt;




&lt;h3&gt;
  
  
  2. The MediaBox vs. CropBox Offset Trap
&lt;/h3&gt;

&lt;p&gt;A standard PDF page does not have just one boundary. It defines up to five distinct bounding boxes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;MediaBox:&lt;/strong&gt; The full physical dimensions of the medium (e.g. letter paper: 612 × 792 points).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CropBox:&lt;/strong&gt; The visible, printable region of the page.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;BleedBox / TrimBox / ArtBox:&lt;/strong&gt; Printing and formatting boundaries.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Many government forms and scanned documents define a &lt;code&gt;MediaBox&lt;/code&gt; starting at &lt;code&gt;(0, 0)&lt;/code&gt;, but a &lt;code&gt;CropBox&lt;/code&gt; with non-zero offsets:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/MediaBox [0 0 612 792]
/CropBox [36 45 576 747]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pure-Python parsers frequently calculate character offsets relative to &lt;code&gt;MediaBox&lt;/code&gt; instead of &lt;code&gt;CropBox&lt;/code&gt;. If the user visualizes the PDF in a viewer (which renders the &lt;code&gt;CropBox&lt;/code&gt;), their defined coordinates will be offset by 30 to 50 points from where the text actually lives!&lt;/p&gt;




&lt;h3&gt;
  
  
  3. Font Metrics and Line Leading vs. Glyph Ink
&lt;/h3&gt;

&lt;p&gt;When extracting text by bounding box, what is the boundary of a word?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Glyph Ink Boundary:&lt;/strong&gt; The exact outline of the visible black pixels.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Font Em-Box Boundary:&lt;/strong&gt; The typographic box including font ascender, descender, and line leading.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If an extraction tool uses only character ink outlines, bounding boxes around lowercase text (e.g., &lt;code&gt;"acme"&lt;/code&gt;) will have completely different vertical heights than text with tall ascenders or descenders (e.g., &lt;code&gt;"plymouth"&lt;/code&gt;). This makes fixed-height template extraction brittle.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Solution: Native C++ Layout Engines
&lt;/h2&gt;

&lt;p&gt;To solve coordinate drift permanently, modern extraction pipelines rely on production-grade C++ PDF rendering engines (like &lt;strong&gt;Google's PDFium&lt;/strong&gt;, the battle-tested engine inside Google Chrome) rather than heuristic pure-Python tokenizers.&lt;/p&gt;

&lt;p&gt;Google PDFium parses native TrueType/OpenType font tables, calculates exact transform matrices, and provides native device-to-page coordinate transformations.&lt;/p&gt;




&lt;h2&gt;
  
  
  Practical Implementation with &lt;code&gt;openfitz&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;To make spatial text extraction and visual validation straightforward in Python, we built &lt;strong&gt;&lt;code&gt;openfitz&lt;/code&gt;&lt;/strong&gt;—a lightweight, permissive (MIT-licensed) library powered directly by Google PDFium.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Installation
&lt;/h3&gt;



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

&lt;/div&gt;






&lt;h3&gt;
  
  
  2. Extracting Text from an Exact Bounding Box
&lt;/h3&gt;

&lt;p&gt;With &lt;code&gt;openfitz&lt;/code&gt;, coordinates use standard top-left &lt;code&gt;(x0, y0, x1, y1)&lt;/code&gt; points, automatically normalized against CropBox boundaries:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;fitz&lt;/span&gt;

&lt;span class="c1"&gt;# Open document
&lt;/span&gt;&lt;span class="n"&gt;doc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fitz&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tax_return.pdf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;doc&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;# Define field bounding box in points: [x0, y0, x1, y1]
&lt;/span&gt;&lt;span class="n"&gt;ein_rect&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fitz&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Rect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;130&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Extract text clipped strictly to this rectangle
&lt;/span&gt;&lt;span class="n"&gt;ein_text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;clip&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;ein_rect&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;strip&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;Extracted EIN:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ein_text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# Output: '12-3456789'
&lt;/span&gt;
&lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  3. Hierarchical Spatial Inspection (&lt;code&gt;blocks&lt;/code&gt;, &lt;code&gt;lines&lt;/code&gt;, &lt;code&gt;spans&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;When you need to reconstruct complex multi-column layouts, inspect font sizes, or parse tables:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;fitz&lt;/span&gt;

&lt;span class="n"&gt;doc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fitz&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;invoice.pdf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;doc&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;# Inspect structured text hierarchy
&lt;/span&gt;&lt;span class="n"&gt;page_dict&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dict&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;block&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;page_dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;blocks&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="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;Block #&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;number&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; at &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;bbox&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;lines&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;span&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;spans&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="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;  Text: &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; | Font: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;font&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; (&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;size&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;pt)&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;Every span contains:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;bbox&lt;/code&gt;: &lt;code&gt;(x0, y0, x1, y1)&lt;/code&gt; in top-left screen points.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;font&lt;/code&gt;: Font family name.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;size&lt;/code&gt;: Typographic font size.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;text&lt;/code&gt;: Extracted UTF-8 string.&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  4. Detecting Checkboxes and Marks via Raster Analysis
&lt;/h3&gt;

&lt;p&gt;One of the hardest problems in form automation is verifying whether a checkbox is checked or blank. Vector text extractors see nothing because checkboxes are often drawn as vector path squares (&lt;code&gt;[ ]&lt;/code&gt;) or images.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;openfitz&lt;/code&gt; solves this by allowing direct rasterization of the target rectangle to memory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;fitz&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;is_checkbox_checked&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;box_coords&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;threshold&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.08&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    Renders the checkbox region and measures darkness ratio.
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="c1"&gt;# 1. Target the checkbox area
&lt;/span&gt;    &lt;span class="n"&gt;box_rect&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fitz&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Rect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;box_coords&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# 2. Render at 2x resolution for crisp pixel sampling
&lt;/span&gt;    &lt;span class="n"&gt;mat&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fitz&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Matrix&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;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;pix&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_pixmap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;matrix&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;mat&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;clip&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;box_rect&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# 3. Analyze raw packed RGB pixel samples
&lt;/span&gt;    &lt;span class="n"&gt;img_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pix&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;samples&lt;/span&gt;
    &lt;span class="n"&gt;total_pixels&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;img_data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;

    &lt;span class="c1"&gt;# Count dark pixels (grayscale value &amp;lt; 200)
&lt;/span&gt;    &lt;span class="n"&gt;dark_pixels&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&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;0&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;img_data&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="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;img_data&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="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;darkness_ratio&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dark_pixels&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;total_pixels&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;total_pixels&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;darkness_ratio&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;threshold&lt;/span&gt;

&lt;span class="c1"&gt;# Usage:
&lt;/span&gt;&lt;span class="n"&gt;doc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fitz&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;w2_form.pdf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;doc&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;# Checkbox at (50, 250, 65, 265)
&lt;/span&gt;&lt;span class="n"&gt;checked&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;is_checkbox_checked&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;250&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;65&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;265&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;Is Box Checked?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;checked&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because rendering is powered by Google's native rasterizer, visual pixel counts match screen rendering with 100% precision.&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusion &amp;amp; Best Practices
&lt;/h2&gt;

&lt;p&gt;When building automated PDF spatial extraction:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Always standardize on top-left origin:&lt;/strong&gt; Normalize all templates to &lt;code&gt;x0, y0, x1, y1&lt;/code&gt; relative to visible page dimensions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Account for non-zero CropBox offsets:&lt;/strong&gt; Never assume &lt;code&gt;(0, 0)&lt;/code&gt; is the physical printable origin.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Combine vector text with raster analysis:&lt;/strong&gt; Use text extraction for typed inputs and raster darkness ratios for checkboxes and signatures.&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;PyPI:&lt;/strong&gt; &lt;a href="https://pypi.org/project/openfitz/" rel="noopener noreferrer"&gt;pypi.org/project/openfitz&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/jagadeesh88705/openfitz" rel="noopener noreferrer"&gt;github.com/jagadeesh88705/openfitz&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;License:&lt;/strong&gt; MIT (Permissive &amp;amp; Commercial-friendly)&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>python</category>
      <category>pymupdf</category>
      <category>pdf</category>
    </item>
    <item>
      <title>How to Handle Duplicate Alembic Migration Numbers When Developers Work in Parallel</title>
      <dc:creator>JAGADEESH .M</dc:creator>
      <pubDate>Fri, 06 Nov 2026 04:14:29 +0000</pubDate>
      <link>https://dev.to/jagadeesh_m_8870/how-to-handle-duplicate-alembic-migration-numbers-when-developers-work-in-parallel-4510</link>
      <guid>https://dev.to/jagadeesh_m_8870/how-to-handle-duplicate-alembic-migration-numbers-when-developers-work-in-parallel-4510</guid>
      <description>&lt;p&gt;If your engineering team uses &lt;strong&gt;SQLAlchemy&lt;/strong&gt; and &lt;strong&gt;Alembic&lt;/strong&gt; to manage database migrations, you have almost certainly encountered this painful scenario:&lt;/p&gt;

&lt;p&gt;Two developers branch off &lt;code&gt;main&lt;/code&gt; at the same time to build different features. Both developers generate an Alembic migration. When their branches are merged or tested on a shared database, Alembic explodes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;alembic.util.exc.CommandError: Multiple head revisions are present for given argument 'head'; please specify a revision or 'heads'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or worse, if you use sequential prefixes like &lt;code&gt;0001_&lt;/code&gt;, &lt;code&gt;0002_&lt;/code&gt;, both developers end up with a file starting with &lt;code&gt;0004_&lt;/code&gt;, causing duplicate revision conflicts, broken CI pipelines, and corrupted shared dev databases.&lt;/p&gt;

&lt;p&gt;In this article, we’ll explore &lt;strong&gt;why&lt;/strong&gt; this happens, why the standard &lt;code&gt;alembic merge&lt;/code&gt; command is often frustrating, and how to automate the resolution cleanly.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Root Cause: Why Parallel Branching Breaks Migration Chains
&lt;/h2&gt;

&lt;p&gt;Alembic represents migration history as a &lt;strong&gt;Directed Acyclic Graph (DAG)&lt;/strong&gt; where each migration script points to its parent via &lt;code&gt;down_revision&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;0001_initial.py  &amp;lt;-  0002_users.py  &amp;lt;-  0003_profiles.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The Conflict Scenario:
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Developer A&lt;/strong&gt; branches off &lt;code&gt;main&lt;/code&gt; at revision &lt;code&gt;0003&lt;/code&gt;. They generate migration &lt;code&gt;0004_add_orders.py&lt;/code&gt; (&lt;code&gt;down_revision = '0003'&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Developer B&lt;/strong&gt; branches off &lt;code&gt;main&lt;/code&gt; at the exact same time. They generate migration &lt;code&gt;0004_add_inventory.py&lt;/code&gt; (&lt;code&gt;down_revision = '0003'&lt;/code&gt;).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Now your migration tree has forked into two heads:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                        ┌──&amp;gt; 0004_add_orders.py (HEAD 1)
0003_profiles.py ───────┤
                        └──&amp;gt; 0004_add_inventory.py (HEAD 2)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When either developer (or your CI/CD pipeline) runs &lt;code&gt;alembic upgrade head&lt;/code&gt;, Alembic halts immediately because it does not know which branch to apply first.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Standard &lt;code&gt;alembic merge&lt;/code&gt; is Problematic for Sequential Migrations
&lt;/h2&gt;

&lt;p&gt;Alembic provides a native command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;alembic merge heads &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"merge orders and inventory"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;While this works, it introduces several major headaches for development teams:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Cluttered "Diamond" Merge Graph:&lt;/strong&gt; It creates a dedicated merge migration file (&lt;code&gt;0005_merge_heads.py&lt;/code&gt;) that contains no actual schema changes—just &lt;code&gt;down_revision = ('0004_orders', '0004_inventory')&lt;/code&gt;. Over a few months with an active team, your migration tree becomes a tangled web of diamond merge nodes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No File Renumbering:&lt;/strong&gt; If your team prefers human-readable sequence prefixes (&lt;code&gt;0001&lt;/code&gt;, &lt;code&gt;0002&lt;/code&gt;, &lt;code&gt;0003&lt;/code&gt;), &lt;code&gt;alembic merge&lt;/code&gt; does &lt;strong&gt;not&lt;/strong&gt; renumber the conflicting files. You are left with two &lt;code&gt;0004_&lt;/code&gt; files permanently in your repository.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shared Database Desynchronization:&lt;/strong&gt; If Developer A tests on a shared dev database, the database gets stamped with &lt;code&gt;0004_orders&lt;/code&gt;. When Developer B pulls the shared database to test their branch, Alembic crashes with &lt;code&gt;Can't locate revision identified by '0004_orders'&lt;/code&gt; because Developer B’s local branch doesn't have Developer A’s file yet.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  The Alternative: Automated In-Place Rebase with &lt;code&gt;alembic-autoseq&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Instead of creating diamond merge nodes or manually renaming files and editing &lt;code&gt;down_revision&lt;/code&gt; by hand, the ideal workflow is an &lt;strong&gt;automated rebase&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;0003_profiles.py -&amp;gt; 0004_add_orders.py -&amp;gt; 0005_add_inventory.py (HEAD)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is what &lt;a href="https://pypi.org/project/alembic-autoseq/" rel="noopener noreferrer"&gt;&lt;code&gt;alembic-autoseq&lt;/code&gt;&lt;/a&gt; does.&lt;/p&gt;

&lt;h3&gt;
  
  
  How It Works:
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;AST Parsing:&lt;/strong&gt; It safely inspects your &lt;code&gt;versions/&lt;/code&gt; folder using Python's native &lt;code&gt;ast&lt;/code&gt; module (without executing migration code or creating database side-effects).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Conflict Detection:&lt;/strong&gt; It identifies multiple heads, duplicate revision IDs, and sequential prefix collisions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Atomic Rebase:&lt;/strong&gt; It determines which migration belongs to the active branch, updates &lt;code&gt;down_revision&lt;/code&gt; to the latest upstream head, auto-increments the sequence number (&lt;code&gt;0004_add_inventory.py&lt;/code&gt; $\rightarrow$ &lt;code&gt;0005_add_inventory.py&lt;/code&gt;), and renames the file on disk.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;In-Memory Cache Reload:&lt;/strong&gt; It refreshes Alembic's in-memory &lt;code&gt;RevisionMap&lt;/code&gt; on the fly so the migration command completes without error.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Step-by-Step Implementation
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Step 1: Install the Package
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;alembic-autoseq
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 2: Add the 1-Line Guard to &lt;code&gt;alembic/env.py&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Inside your project's &lt;code&gt;alembic/env.py&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="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;alembic_autoseq&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run_migrations_online&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;connectable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;engine_from_config&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt;

    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;connectable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# Intercepts and heals parallel branch conflicts before Alembic runs
&lt;/span&gt;        &lt;span class="n"&gt;alembic_autoseq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;guard&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;connection&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;auto_heal&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;configure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;connection&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;target_metadata&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;target_metadata&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;begin_transaction&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
            &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run_migrations&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 3: Run Your Normal Migration
&lt;/h3&gt;

&lt;p&gt;Now, developers simply run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;alembic upgrade &lt;span class="nb"&gt;head&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If a colleague merged a migration while they were working, &lt;code&gt;alembic-autoseq&lt;/code&gt; automatically linearizes the branch migration and applies it cleanly.&lt;/p&gt;




&lt;h2&gt;
  
  
  Adding a CI/CD Quality Gate
&lt;/h2&gt;

&lt;p&gt;To ensure that no conflicting or multi-head migrations are ever merged into &lt;code&gt;main&lt;/code&gt;, you can add a check to your GitHub Actions pipeline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .github/workflows/migrations.yml&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Migration Check&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;verify-migrations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-python@v5&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;python-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3.11"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pip install alembic-autoseq&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;alembic-autoseq check&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If a Pull Request introduces a duplicate revision ID or a multi-head fork, &lt;code&gt;alembic-autoseq check&lt;/code&gt; will exit with code &lt;code&gt;1&lt;/code&gt; and display a clear error message in the PR logs.&lt;/p&gt;




&lt;h2&gt;
  
  
  When NOT to Rebase
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Production Migrations:&lt;/strong&gt; Never modify migrations that have already been applied to your production database. &lt;code&gt;alembic-autoseq&lt;/code&gt; is strictly designed for &lt;strong&gt;unmerged, local feature branches&lt;/strong&gt; before code is merged into your production release branch.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Intentional Multi-Database Branching:&lt;/strong&gt; If your infrastructure deliberately uses separate Alembic branches for multi-tenant or multi-database architectures, stick to Alembic’s native branching.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Parallel development shouldn't mean wasting 30 minutes resolving migration merge conflicts or cluttering your Git history with empty merge commits. By adopting an automated sequential rebase strategy, teams can maintain clean, linear database migration histories effortlessly.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;📦 &lt;strong&gt;PyPI:&lt;/strong&gt; &lt;a href="https://pypi.org/project/alembic-autoseq/" rel="noopener noreferrer"&gt;&lt;code&gt;pip install alembic-autoseq&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;💻 &lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/jagadeesh88705/alembic_autoseq" rel="noopener noreferrer"&gt;https://github.com/jagadeesh88705/alembic_autoseq&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>python</category>
      <category>programming</category>
      <category>alembic</category>
      <category>sqlalchemy</category>
    </item>
  </channel>
</rss>
