Python Barcode Generation — Deep Dive

How 1D barcodes encode data

A 1D barcode is a sequence of bars and spaces of varying widths. Each symbology defines a mapping from characters to bar/space patterns:

EAN-13 encoding:

The 13-digit code is split into a leading digit, a left group of 6 digits, and a right group of 6 digits. The leading digit determines which of two encoding sets (L and G) each left-group digit uses. Right-group digits always use the R encoding. Each digit maps to a 7-module pattern (e.g., “0” in L-encoding = 0001101).

The result is framed by start, center, and end guard patterns — fixed bar sequences that tell the scanner where groups begin and end.

Code 128 encoding:

Code 128 uses three character sets (A: control + uppercase, B: full printable ASCII, C: digit pairs). The encoder starts with a start character indicating the initial set, then switches sets mid-stream as needed with shift characters. Each data character is 11 modules wide (3 bars + 3 spaces). A modulo-103 check character precedes the stop pattern.

The python-barcode library implements these algorithms in pure Python. The encode() method returns a string of 1s and 0s representing bar and space modules, which the writer renders into an image.

Custom writers

The library’s writer system is extensible. Built-in writers include SVGWriter (default) and ImageWriter (Pillow-based). Create a custom writer by subclassing BaseWriter:

from barcode.writer import BaseWriter

class JSONWriter(BaseWriter):
    def __init__(self):
        super().__init__(self._init, self._paint_module, self._paint_text, self._finish)
        self.modules = []
    
    def _init(self, code):
        self.modules = []
    
    def _paint_module(self, xpos, ypos, width, color):
        self.modules.append({
            'x': xpos, 'y': ypos,
            'width': width, 'color': color
        })
    
    def _paint_text(self, xpos, ypos):
        pass  # skip text rendering
    
    def _finish(self):
        import json
        return json.dumps(self.modules)

This pattern is useful for generating barcodes in custom formats — for example, producing drawing commands for a thermal printer SDK or generating coordinates for a laser engraver.

GS1 compliance

GS1 is the international standards body for barcodes in retail and logistics. Compliance requires:

  • Correct symbology — EAN-13 for retail, GS1-128 for logistics
  • Proper sizing — EAN-13 nominal size is 37.29mm × 25.93mm; allowed magnification range is 80%-200%
  • X-dimension — the width of the narrowest bar. EAN-13 nominal is 0.33mm; minimum is 0.264mm
  • Quiet zones — minimum white space on each side (EAN-13 requires 11 modules left, 7 modules right)
  • Bar height — minimum 22.85mm at 100% magnification

Configure python-barcode for GS1 compliance:

options = {
    'module_width': 0.33,      # GS1 nominal X-dimension in mm
    'module_height': 22.85,    # GS1 minimum bar height
    'quiet_zone': 3.63,       # 11 × 0.33mm for EAN-13 left zone
    'dpi': 300,
}

For GS1-128 (used in shipping labels), encode Application Identifiers (AIs) within the data:

# AI (01) = GTIN, AI (17) = Expiry date, AI (10) = Batch
data = "(01)09501234567890(17)260401(10)BATCH123"
# Strip parentheses for encoding; they're human-readable only
encoded_data = "0109501234567890172604011012BATCH123"
code = barcode.get('code128', encoded_data, writer=ImageWriter())

Label sheet generation

For printing on standard label sheets (Avery 5160, etc.), use reportlab to lay out barcodes in a grid:

from reportlab.lib.pagesizes import letter
from reportlab.lib.units import inch, mm
from reportlab.pdfgen import canvas
from reportlab.graphics.barcode import code128
import barcode as pybarcode
from io import BytesIO
from PIL import Image

def generate_label_sheet(items, output_pdf, labels_per_row=3, labels_per_col=10):
    c = canvas.Canvas(output_pdf, pagesize=letter)
    page_w, page_h = letter
    
    margin_x, margin_y = 0.19 * inch, 0.5 * inch
    label_w = (page_w - 2 * margin_x) / labels_per_row
    label_h = (page_h - 2 * margin_y) / labels_per_col
    
    for idx, item in enumerate(items):
        if idx > 0 and idx % (labels_per_row * labels_per_col) == 0:
            c.showPage()
        
        pos = idx % (labels_per_row * labels_per_col)
        col = pos % labels_per_row
        row = pos // labels_per_row
        
        x = margin_x + col * label_w + 5 * mm
        y = page_h - margin_y - (row + 1) * label_h + 5 * mm
        
        bc = code128.Code128(item['code'], barWidth=0.3*mm, barHeight=10*mm)
        bc.drawOn(c, x, y + 5 * mm)
        c.setFont("Helvetica", 7)
        c.drawString(x, y, item['name'][:30])
    
    c.save()

High-volume generation

For generating millions of barcodes (warehouse inventory, event tickets):

from concurrent.futures import ProcessPoolExecutor
import barcode
from barcode.writer import ImageWriter

def generate_one(args):
    code_data, output_path = args
    bc = barcode.get('code128', code_data, writer=ImageWriter())
    bc.save(output_path)
    return output_path

items = [(f"ITEM-{i:08d}", f"output/barcode_{i:08d}") for i in range(100000)]

with ProcessPoolExecutor(max_workers=8) as executor:
    results = list(executor.map(generate_one, items))

Performance tips:

  • SVG is faster than PNG — no rasterization step
  • Process pool > thread pool — barcode generation is CPU-bound (GIL limitation)
  • Batch to PDF — generating one PDF with 1000 barcodes is faster than 1000 individual images
  • Cache the writer — reusing a single ImageWriter avoids repeated Pillow initialization

Thermal printer integration

Thermal label printers (Zebra, DYMO, Brother) often accept ZPL (Zebra Programming Language) or ESC/POS commands. Generate barcodes directly in these formats:

# ZPL barcode command for Zebra printers
def to_zpl(data, barcode_type='C', height=100):
    return f"""
^XA
^FO50,50
^BY2
^B{barcode_type},{height},Y,N,N
^FD{data}^FS
^XZ
"""

# Send to printer via network
import socket
def print_zpl(zpl_string, printer_ip, port=9100):
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
        s.connect((printer_ip, port))
        s.send(zpl_string.encode())

For DYMO printers, use the dymoprint library or generate images at the printer’s native DPI (usually 300) and send via the CUPS driver.

2D barcode alternatives

While python-barcode focuses on 1D barcodes, production systems often need 2D formats:

  • QR Code — use the qrcode library (see python-qrcode-generation)
  • Data Matrix — use pylibdmtx for encoding and decoding
  • PDF417 — use pdf417gen for this stacked 2D format common in ID cards and boarding passes
  • Aztec — less common in Python; consider zxing via subprocess

Scanner integration and validation

For automated quality control, scan generated barcodes programmatically:

from pyzbar.pyzbar import decode
from PIL import Image

def validate_barcode(image_path, expected_data, expected_type='EAN13'):
    img = Image.open(image_path)
    results = decode(img)
    
    if not results:
        return False, "No barcode detected"
    
    for result in results:
        if result.type == expected_type and result.data.decode() == expected_data:
            return True, "Valid"
    
    actual = [(r.type, r.data.decode()) for r in results]
    return False, f"Mismatch: expected {expected_type}/{expected_data}, got {actual}"

Quality metrics to check:

  • Decode success rate — scan each barcode at least 3 times from different angles
  • Quiet zone compliance — ensure sufficient white space around the barcode
  • Print contrast signal (PCS) — measure the difference between bar and space reflectance
  • Edge determination — verify bars have sharp, clean edges at the target DPI

Error handling patterns

import barcode

def safe_generate(symbology, data, output_path, **options):
    try:
        bc_class = barcode.get_barcode_class(symbology)
    except barcode.errors.BarcodeNotFoundError:
        raise ValueError(f"Unknown symbology: {symbology}")
    
    try:
        bc = bc_class(data, writer=ImageWriter())
    except barcode.errors.NumberOfDigitsError as e:
        raise ValueError(f"Invalid data length for {symbology}: {e}")
    except barcode.errors.IllegalCharacterError as e:
        raise ValueError(f"Invalid character for {symbology}: {e}")
    
    return bc.save(output_path, options=options)

Common errors:

  • Wrong digit count for fixed-length symbologies (EAN-13 needs 12 or 13 digits)
  • Non-numeric characters in numeric-only symbologies
  • Check digit mismatch when providing the full code
  • Missing Pillow when using ImageWriter

The one thing to remember: Production barcode generation requires GS1 compliance awareness, programmatic scan validation, proper sizing for the target print medium, and error handling for data format mismatches — the library handles encoding, but the system around it determines whether the printed result actually scans reliably.

pythonbarcodegenerationretailgs1logistics

See Also

  • Python Arcade Library Think of a magical art table that draws your game characters, listens when you press buttons, and cleans up the mess — that's Python Arcade.
  • Python Audio Fingerprinting Ever wonder how Shazam identifies a song from just a few seconds of noisy audio? Audio fingerprinting is the magic behind it, and Python can do it too.
  • Python Cellular Automata Imagine a checkerboard where each square follows simple rules to turn on or off — and suddenly complex patterns emerge like magic.
  • Python Godot Gdscript Bridge Imagine speaking English to a friend who speaks French, with a translator in the middle — that's how Python talks to the Godot game engine.
  • Python Librosa Audio Analysis Picture a music detective that can look at any song and tell you exactly what notes, beats, and moods are hiding inside — that's what Librosa does for Python.