Warehouse Management in Python — Deep Dive

Building warehouse management logic in Python means solving spatial optimization problems, integrating with barcode/RFID hardware, and serving real-time decisions to workers on the floor. This guide covers the technical implementations behind slotting, pick routing, wave planning, and inventory reconciliation.

Warehouse layout modeling

Before optimizing anything, model the physical warehouse as a graph:

from dataclasses import dataclass
import networkx as nx

@dataclass
class Location:
    loc_id: str
    aisle: int
    bay: int
    level: int
    zone: str
    x: float  # meters from origin
    y: float

def build_warehouse_graph(locations: list[Location], aisle_width: float = 3.0) -> nx.Graph:
    """Build a traversal graph where nodes are pick locations and edges are walkable paths."""
    G = nx.Graph()
    for loc in locations:
        G.add_node(loc.loc_id, pos=(loc.x, loc.y), zone=loc.zone)

    # Connect locations within the same aisle sequentially
    by_aisle = {}
    for loc in locations:
        by_aisle.setdefault(loc.aisle, []).append(loc)

    for aisle, locs in by_aisle.items():
        locs.sort(key=lambda l: l.y)
        for i in range(len(locs) - 1):
            dist = abs(locs[i].y - locs[i+1].y)
            G.add_edge(locs[i].loc_id, locs[i+1].loc_id, weight=dist)

    # Connect aisle ends (cross-aisles at front and back)
    aisle_ends = {}
    for aisle, locs in by_aisle.items():
        aisle_ends[aisle] = (locs[0], locs[-1])

    sorted_aisles = sorted(aisle_ends.keys())
    for i in range(len(sorted_aisles) - 1):
        a1, a2 = sorted_aisles[i], sorted_aisles[i+1]
        # Front cross-aisle
        G.add_edge(aisle_ends[a1][0].loc_id, aisle_ends[a2][0].loc_id, weight=aisle_width)
        # Back cross-aisle
        G.add_edge(aisle_ends[a1][1].loc_id, aisle_ends[a2][1].loc_id, weight=aisle_width)

    return G

Slotting optimization

Velocity-based slotting assigns the fastest-moving SKUs to the most accessible locations (closest to the packing area, at pick-height level):

import pandas as pd
from scipy.optimize import linear_sum_assignment

def optimize_slotting(
    sku_velocity: pd.DataFrame,   # columns: sku, picks_per_week
    locations: pd.DataFrame,       # columns: loc_id, distance_to_pack, ergonomic_score
) -> pd.DataFrame:
    """Assign SKUs to locations minimizing total weighted travel distance."""
    # Rank SKUs by velocity (highest first)
    skus = sku_velocity.sort_values("picks_per_week", ascending=False).reset_index(drop=True)
    # Rank locations by desirability (closest and most ergonomic first)
    locs = locations.sort_values(["distance_to_pack", "ergonomic_score"]).reset_index(drop=True)

    n = min(len(skus), len(locs))
    # Build cost matrix: high-velocity SKU in far location = high cost
    cost_matrix = np.zeros((n, n))
    for i in range(n):
        for j in range(n):
            cost_matrix[i][j] = skus.iloc[i]["picks_per_week"] * locs.iloc[j]["distance_to_pack"]

    row_ind, col_ind = linear_sum_assignment(cost_matrix)

    assignments = []
    for r, c in zip(row_ind, col_ind):
        assignments.append({"sku": skus.iloc[r]["sku"], "loc_id": locs.iloc[c]["loc_id"]})

    return pd.DataFrame(assignments)

For warehouses with 10,000+ SKUs and locations, the Hungarian algorithm in linear_sum_assignment solves in seconds. Re-run monthly as velocity patterns shift.

Pick-path routing

Given a set of locations to visit, find the shortest walking path through the warehouse graph:

def optimal_pick_path(
    graph: nx.Graph,
    pick_locations: list[str],
    start: str = "PACK_STATION",
) -> tuple[list[str], float]:
    """Solve TSP on warehouse graph for a pick list."""
    nodes = [start] + pick_locations
    # Build distance sub-matrix using shortest paths
    dist = {}
    for i, u in enumerate(nodes):
        lengths = nx.single_source_dijkstra_path_length(graph, u, weight="weight")
        for j, v in enumerate(nodes):
            dist[(i, j)] = lengths.get(v, float("inf"))

    # Nearest-neighbor heuristic + 2-opt improvement
    n = len(nodes)
    visited = [False] * n
    path = [0]
    visited[0] = True

    for _ in range(n - 1):
        last = path[-1]
        nearest = min(
            (j for j in range(n) if not visited[j]),
            key=lambda j: dist[(last, j)],
        )
        path.append(nearest)
        visited[nearest] = True

    # 2-opt improvement
    improved = True
    while improved:
        improved = False
        for i in range(1, len(path) - 1):
            for j in range(i + 1, len(path)):
                old_cost = dist[(path[i-1], path[i])] + dist[(path[j-1], path[j])]
                new_cost = dist[(path[i-1], path[j-1])] + dist[(path[i], path[j])]
                if new_cost < old_cost:
                    path[i:j] = reversed(path[i:j])
                    improved = True

    total_dist = sum(dist[(path[i], path[i+1])] for i in range(len(path) - 1))
    ordered_locations = [nodes[i] for i in path]
    return ordered_locations, total_dist

For batch picking (multiple orders in one trip), merge the pick lists and solve once, then separate items by order at the sorting station.

Wave planning

Wave planning groups orders into batches that release together. Objectives:

  • Meet carrier cutoff times (e.g., FedEx pickup at 3 PM).
  • Balance workload across zones.
  • Maximize batch-pick efficiency (orders sharing many SKUs go in the same wave).
from collections import defaultdict

def plan_waves(
    orders: list[dict],         # each: {order_id, items: [sku, ...], carrier, cutoff_time}
    max_wave_size: int = 50,
    max_waves: int = 10,
) -> list[list[str]]:
    """Group orders into waves by carrier cutoff and SKU overlap."""
    # Sort by cutoff urgency
    orders.sort(key=lambda o: o["cutoff_time"])

    waves = []
    current_wave = []
    current_skus = set()

    for order in orders:
        order_skus = set(order["items"])
        overlap = len(order_skus & current_skus) / max(1, len(order_skus))

        if len(current_wave) >= max_wave_size:
            waves.append([o["order_id"] for o in current_wave])
            current_wave = []
            current_skus = set()

        current_wave.append(order)
        current_skus |= order_skus

    if current_wave:
        waves.append([o["order_id"] for o in current_wave])

    return waves[:max_waves]

Production systems use more sophisticated clustering (K-Medoids on SKU overlap distance) and constraint programming to respect zone capacity limits.

Real-time inventory tracking

A FastAPI service handles barcode scan events from handheld devices:

from fastapi import FastAPI
from pydantic import BaseModel
from datetime import datetime

app = FastAPI()

class ScanEvent(BaseModel):
    location_id: str
    sku: str
    quantity: int
    event_type: str  # "receive", "pick", "move", "count"
    worker_id: str
    timestamp: datetime

@app.post("/scan")
async def process_scan(event: ScanEvent):
    if event.event_type == "receive":
        await update_inventory(event.location_id, event.sku, event.quantity)
    elif event.event_type == "pick":
        await update_inventory(event.location_id, event.sku, -event.quantity)
    elif event.event_type == "move":
        # Move triggers two updates: remove from source, add to destination
        pass
    elif event.event_type == "count":
        await reconcile_count(event.location_id, event.sku, event.quantity)

    return {"status": "ok", "processed_at": datetime.utcnow()}

Events stream into a PostgreSQL inventory table and a Redis cache for sub-second lookups during pick-path computation.

Measuring improvement

Before/after metrics for a Python-augmented WMS:

MetricBefore (manual)After (Python-optimized)
Pick rate (lines/hour)85130
Walk distance per order180 m110 m
Slotting review frequencyAnnualMonthly
Inventory accuracy96.5%99.2%
Wave planning time45 min (manual)2 min (automated)

Pitfalls

  • Static slotting — velocity changes seasonally. A product that was fast-moving six months ago may now be a slow mover occupying prime real estate.
  • Ignoring congestion — the shortest path means nothing if three pickers converge on the same aisle. Stagger wave releases or add congestion penalties to the routing graph.
  • Over-engineering for small warehouses — a 500-location warehouse does not need a full graph solver. Simple zone rules and manual slotting reviews often suffice.
  • Barcode scan failures — if workers skip scans when busy, inventory accuracy degrades quickly. Design the system to flag missing scan sequences.

The one thing to remember: Python warehouse management combines spatial graph modeling for pick routing, assignment optimization for slotting, wave planning for batch efficiency, and real-time scan processing to turn warehouse operations from intuition-driven to data-driven.

pythonwarehouselogisticsautomation

See Also

  • Python Adaptive Learning Systems How Python builds learning apps that adjust to each student like a personal tutor who knows exactly what you need next.
  • Python Airflow Learn Airflow as a timetable manager that makes sure data tasks run in the right order every day.
  • Python Altair Learn Altair through the idea of drawing charts by describing rules, not by hand-placing every visual element.
  • Python Automated Grading How Python grades homework and exams automatically, from simple answer keys to understanding written essays.
  • Python Batch Vs Stream Processing Batch processing is like doing laundry once a week; stream processing is like a self-cleaning shirt that cleans itself constantly.