From Logistics Challenges to Python Solutions: Insights on Data Structures

Nabin GaraiNabin Garai
13 min read

Just a month ago, I was part of a last-mile operations team at a logistics company. Each morning, I would head to the Dispatch Centre (DC) to start my day. The load vehicle would arrive, packed with shipment bags. We would break each bag’s seal and scan each shipment's Air Waybill (AWB) to update the server with a list of received shipments, which also updates the portal to show that the shipment has reached the customer’s nearby dispatch centre. Last week, while Priya ma’am was teaching data structures in Python, the concepts vividly reminded me of the real-world logistics processes I had worked with. Our load vehicle was like a list arriving at the DC full of fresh shipment bags. We would unload these bags and load the return shipment bags from the DC. The pincodes we served were like a Tuple, which was unchangeable. Each shipment had an AWB number, customer name, address, and PIN code, which reminds me of a Dictionary. Set reminds me of creating the OFD (Out For Dispatch) runsheet for the Field Executives (Delivery Personnel), where we can't add an AWB multiple times because the runsheet includes unique AWBs only. This experience taught me something important:

Organizing shipments in the DC is like organizing data in Python. The better your data structures, the smoother everything works.

In this blog, I'll guide you through how Python's basic data structures—lists, dictionaries, sets, and tuples—relate directly to real-world logistics operations.

Let's Dive Deeper into the Details.

🚚 List

Imagine the load vehicle as a list of shipment bags arriving at the DC. When the vehicle arrives, we unload the bags for the DC (like taking items out of a list) and load the return shipment bags into the vehicle (like adding items to a list).

This is similar to how we use lists in Python, where we can add, remove, access, and change items in the list throughout the process. The list is mutable, which means we can change it as needed.

dc_load = []    # empty list
load_vehicle = ["Bag1","Bag2","Bag3", "BagL001", "BagL002", "Bag4", "Bag5", "BagL003", "BagL004"] #load_vehicles contains bags/items

📦 Unloading

When the load vehicle arrives at our Distribution Centre (DC), we open it, just like accessing a list in Python. We then unload the shipment bags, like taking items from a Python list. We can use methods like remove() deleting a specific item by value, pop() removing an item at a particular index, or clear() emptying the list. This flexibility helps us manage the unloading process efficiently, making sure each bag is handled properly.

load_vehicle = ["Bag1","Bag2","Bag3", "BagL001", "BagL002", "Bag4", "Bag5", "BagL003", "BagL004"]

load_vehicle.pop() # .pop() remove the items from end and return it
load_vehicle.pop(7) # .pop(index) remove the item from given index
load_vehicle.remove("BagL002") # .remove(item) remove the given item
load_vehicle.remove("Bag4")
del load_vehicle[7] # del delete the item from given index
load_vehicle.clear() #.clear remove the all items from the list

📦 Loading

When we have return shipment bags or large return shipments ready to be sent back, we proceed to load them onto the vehicle after the unloading process is complete. This involves carefully organising and placing each return bag into the vehicle to ensure efficient use of space and secure transportation. The process is similar to adding items to a list in Python, where we append new elements to the list. By doing so, we ensure that all return items are accounted for and properly loaded, ready for their journey back to the origin or another destination. This step is crucial for maintaining the flow of shipments and ensuring that all returns are handled promptly and efficiently.

load_vehicle = []
large_return = ['RTBagL001', 'RTBagL002', 'RTBagL003']

load_vehicle.append('RTBag1')  # --
#                                 | --------> .append() add item to the list and return it
load_vehicle.append('RTBag2')  # --
print(load_vehicle) # Output: ['RTBag1', 'RTBag2']

load_vehicle.extend(large_return) # .extend() appened multiple items to the list but return none
print(load_vehicle) # Output: ['RTBag1', 'RTBag2', 'RTBagL001', 'RTBagL002', 'RTBagL003']

load_vehicle.insert(3, "RTBag3")  # .insert(index, item) appened particular item to the given index
print(load_vehicle) # Output: ['RTBag1', 'RTBag2', 'RTBagL001', 'RTBag3', 'RTBagL002', 'RTBagL003']

⚒ Additional List Operations

When working with lists in Python, there are several other operations you can perform to manipulate and manage the data effectively. Here are some of the most commonly used list operations:

  1. Slicing Operation: In Python, slicing lets you access parts of a list by specifying a range of indices in square brackets. The syntax is list[start:stop:step]where start the beginning index stop is the end index (exclusive), and step is the interval. For example, you can use slicing to extract portions of a list like load_vehicle.

     load_vehicle = ['bag1', 'bag2', 'bag3', 'bag4', 'bag5']
    
     # Slicing to get elements from index 1 to index 3
     sliced_list_1 = load_vehicle[1:4]
     print("Sliced list from index 1 to 3:", sliced_list_1)  # Output: ['bag2', 'bag3', 'bag4']
    
     # Slicing with a step to skip every other element
     sliced_list_2 = load_vehicle[0:5:2]
     print("Sliced list with every other element:", sliced_list_2)  # Output: ['bag1', 'bag3', 'bag5']
    
  2. Sorting Lists: To sort the items in a list, you can use the .sort() method, which sorts the list in place, or the sorted() function, which returns a new sorted list without modifying the original.

     load_vehicle = ['RTBag1', 'RTBag2', 'RTBagL001', 'RTBag3', 'RTBagL002', 'RTBagL003']  
     load_vehicle.sort() # Sorts the list in ascending order
     print(load_vehicle)  # Output: ['RTBag1', 'RTBag2', 'RTBag3', 'RTBagL001', 'RTBagL002', 'RTBagL003']
    
     sorted_list = sorted(load_vehicle, reverse=True)  # Returns a new list sorted in descending order
     print(sorted_list)  # Output: ['RTBagL003', 'RTBagL002', 'RTBagL001', 'RTBag3', 'RTBag2', 'RTBag1']
    
  3. Reversing Lists: You can reverse the order of items in a list using the .reverse() method, which modifies the list in place, or by using slicing.

     load_vehicle.reverse()  # Reverses the order of the list
     print(load_vehicle)  # Output: ['RTBagL003', 'RTBagL002', 'RTBagL001', 'RTBag3', 'RTBag2', 'RTBag1']
    
     reversed_list = load_vehicle[::-1]  # Creates a new list that is the reverse of the original
     print(reversed_list)  # Output: ['RTBag1', 'RTBag2', 'RTBag3', 'RTBagL001', 'RTBagL002', 'RTBagL003']
    
  4. Finding Elements: To find the position of an item in a list, use the .index(item) method, which returns the index of the first occurrence of the specified value.

     index_of_item = load_vehicle.index('RTBag3')
     print(index_of_item)  # Output: 3
    

These operations provide powerful tools for managing and manipulating lists, allowing you to handle data efficiently and effectively in your Python programs.

🚚 Tuple

The pincodes we used were like a Tuple, a data structure in Python that can't be changed. Once you create a tuple, you can't change, add, or remove its items. This makes tuples perfect for keeping data that shouldn't change while a program runs. For us, the list of pincodes had to stay the same, making sure the data was consistent and reliable. By using a tuple, we could manage this data easily, knowing it would stay intact.

Tuples are initialised with parentheses (). Tuples are faster than lists.

serving_pincodes = (560067, 560066, 560061, 560043)

📦 Checking a Pincode (Accessing Elements)

To check if a pincode is in your service area, use the tuple of pincodes you serve. Since tuples can't be changed, they reliably store constant data. Verifying a pincode in the tuple ensures accurate and efficient service area checks, maintaining consistent and dependable data.

print(serving_pincodes[0])  # Output: 560067
print(serving_pincodes[2])  # Output: 560061

📦 Converting a Tuple to a List

Sometimes, you may need to change data in a tuple, even though tuples are immutable. You can convert the tuple to a list using the list() function, which allows you to add, remove, or update elements. This is helpful when you need the flexibility of a list after initially using a tuple for its immutability.

Here's an example of how you can convert a tuple of pincodes to a list and then modify it:

# Original tuple of pincodes
serving_pincodes = (560067, 560066, 560061, 560043)

# Convert the tuple to a list
pincode_list = list(serving_pincodes)

# Now you can modify the list
pincode_list.append(560068)  # Add a new pincode
pincode_list.remove(560043)  # Remove an existing pincode

print(pincode_list)  # Output: [560067, 560066, 560061, 560068]

By converting a tuple to a list, you gain the flexibility to manage and update your data as your program's requirements evolve, while still benefiting from the initial immutability of tuples when needed.

📦Built-in Functions

Even though tuples are immutable in Python, meaning you can’t change their contents once created, Python provides several built-in functions to work with them efficiently. These functions allow you to inspect, analyse, and convert tuples, which is useful for handling fixed data like service pincodes in logistics.

Here's a simple overview of the most useful built-in functions for tuples.

# Tuple of serving pincodes
serving_pincodes = (560067, 560066, 560061, 560043)

# 1. len() - Count of pincodes
print(len(serving_pincodes))  
# Output: 4

# 2. min() - Smallest pincode
print(min(serving_pincodes))  
# Output: 560043

# 3. max() - Largest pincode
print(max(serving_pincodes))  
# Output: 560067

# 4. sum() - Total of all pincodes
print(sum(serving_pincodes))  
# Output: 2240237

# 5. sorted() - Sorted version (returns a list)
print(sorted(serving_pincodes))  
# Output: [560043, 560061, 560066, 560067]

# 6. tuple() - Convert list to tuple
pin_list = [560001, 560002, 560003]
converted_tuple = tuple(pin_list)
print(converted_tuple)  
# Output: (560001, 560002, 560003)

# 7. count() - Count how many times a pincode appears
pincodes_with_duplicates = (560066, 560066, 560067, 560043)
print(pincodes_with_duplicates.count(560066))  
# Output: 2

# 8. index() - Get index of a pincode
print(serving_pincodes.index(560061))  
# Output: 2

Once your DC gets approval for certain service zones (like 560067, 560066, etc.), those zones are fixed — you can't change them daily. Similarly, a tuple keeps these values unchanged, ensuring data safety and consistency.

🚚 Set

Set reminds me of creating the OFD (Out For Dispatch) runsheet for Field Executives, who are the Delivery Personnel. It's important not to add an AWB (Air Waybill) number more than once because the runsheet should only have unique AWB numbers. This ensures each package is included without duplicates. This is like a set in programming, where each item is unique and duplicates aren't allowed, keeping the data accurate and reliable.

A set is a collection of unique elements, meaning there will be no repeated elements. Sets are unordered and unindexed, so elements don't have a specific order and can't be accessed by index. However, sets are mutable, allowing you to add or remove elements. This makes them useful for storing distinct items and performing operations like union, intersection, and difference. The set is initialised as {}.

📦Adding to Runsheet

In a DC, before going out for delivery, every FE must create a runsheet—a list of unique AWBs assigned to them for delivery that day. When a new shipment is assigned to the FE, we can use the add() method to update their runsheet.

runsheet_fe1 = {}  # Empty runsheet

# AWBs assigning to FE1
runsheet_fe1.add(1001)
runsheet_fe1.add(1002)
runsheet_fe1.add(1003)
runsheet_fe1.add(1004)

print( runsheet_fe1) # Output: {1001, 1002, 1003, 1004}

If we try to add an AWB that has already been added, the set will ignore it.

runsheet_fe1.add(1002)  # No error, just ignored
print(runsheet_awbs)    # Output: {1001, 1002, 1003, 1004}

📦 Removing From Runsheet

If a FE adds a shipment that doesn't belong to their route but to another FE, we need to remove that shipment from their runsheet. To remove a shipment from the FE's runsheet, we can use the remove() method to update it.

runsheet_fe1 = {1001, 1002, 1003, 1004}  # Existing runsheet

# Removing an AWB from FE1's runsheet
runsheet_fe1.remove(1002)

print(runsheet_fe1)  # Output: {1001, 1003, 1004}

If you try to remove an AWB that isn't in the set, it will cause an error. To avoid this, you can use the discard() method, which won't raise an error if the item isn't found.

runsheet_fe1.discard(1005)  # No error, even though 1005 isn't in the set
print(runsheet_fe1)         # Output: {1001, 1003, 1004}

This method removes a random item and returns it.

print(runsheet_fe1.pop())    #Output: 1001

This method removes all items from the set, leaving it empty.

runsheet_fe1 = {1001, 1003, 1004}
runsheet_fe1.clear()
print(runsheet_fe1)  #Output: set() --> An Empty Set

⚒ Set Operations

  1. union() or | : Combines elements from two sets without duplicates.

     runsheet1_fe1 = {1001, 1002, 1003, 1004}
     runsheet2_fe1 = {1004, 1005, 1003, 1006} 
    
     print(runsheet1_fe1 | runsheet2_fe1)  # Output: {1001, 1002, 1003, 1004, 1005, 1006}
                     # or
     print(runsheet1_fe1.union(runsheet2_fe1))  # Output: {1001, 1002, 1003, 1004, 1005, 1006}
    
  2. intersection() or & : Returns the elements that are common between two sets.

     runsheet1_fe1 = {1001, 1002, 1003, 1004}
     runsheet2_fe1 = {1004, 1005, 1003, 1006} 
    
     print(runsheet1_fe1 & runsheet2_fe1)  # Output: {1003, 1004}
                     # or
     print(runsheet1_fe1.intersection(runsheet2_fe1))  # Output: {1003, 1004}
    
  3. difference() or - : Finds elements that are in the first set but not in the second set.

     runsheet1_fe1 = {1001, 1002, 1003, 1004}
     runsheet2_fe1 = {1004, 1005, 1003, 1006} 
    
     print(runsheet1_fe1 - runsheet2_fe1)  # Output: {1001, 1002}
                     # or
     print(runsheet1_fe1.difference(runsheet2_fe1))  # Output: {1001, 1002}
    
  4. symmetric_difference() or ^ : Finds elements that are in either of the two sets but not in both.

     runsheet1_fe1 = {1001, 1002, 1003, 1004}
     runsheet2_fe1 = {1004, 1005, 1003, 1006} 
    
     print(runsheet1_fe1 ^ runsheet2_fe1)  # Output: {1001, 1002, 1005, 1006}
                     # or
     print(runsheet1_fe1.symmetric_difference(runsheet2_fe1))  # Output: {1001, 1002, 1005, 1006}
    
  5. issubset() : It checks if all elements of one set are present in another set. If they are, it returns True; otherwise, it returns False.

     runsheet1_fe1 = {1001, 1002}
     runsheet2_fe1 = {1001, 1002, 1003, 1004}
    
     print(runsheet1_fe1.issubset(runsheet2_fe1))  # Output: True
    
  6. issuperset() : This checks if all elements of another set are present in the first set. If they are, it returns True; otherwise, it returns False.

     runsheet1_fe1 = {1001, 1002}
     runsheet2_fe1 = {1001, 1002, 1003, 1004}
    
     print(runsheet1_fe1.issuperset(runsheet2_fe1))  # Output: False
    
  7. isdisjoint() : This method checks if two sets have no elements in common. If they don't share any elements, it returns True; otherwise, it returns False.

     runsheet1_fe1 = {1001, 1002}
     runsheet2_fe1 = {1003, 1004}
    
     print(runsheet1_fe1.isdisjoint(runsheet2_fe1))  # Output: True
    

    🚚Dictionary

    Each shipment record had important details: an Air Waybill (AWB) number, the customer's name, their address, and the PIN code. This way of storing data is like a Dictionary in programming. In a dictionary, data is stored in key-value pairs, making it easy to find and manage information. Similarly, each shipment's details can be seen as a dictionary entry, where the AWB number is a unique key, and the customer name, address, and PIN code are the values. This shows how dictionaries are useful for organising and accessing complex data in an organised way.

     shipment1 = {
         'AWB' : 1001,
         'Customer_name': "Alex",
         'Mobile' : 9999999999,
         'Address' : "Kadugodi, Bangalore, KA",
         'PIN' : 560067,
     }
    

    📦 get( )

    This method in a dictionary is used to access the value for a given key. If the key exists, it returns the value associated with it. If the key is not found, it returns None by default.

     print(shipment1.get('Customer'))     # Output: Alex
     print(shipment1.get('Price'))   # Output: None
    

    📦 keys( )

    This method in a dictionary is used to get a list of all the keys in the dictionary.

     print(shipment1.keys())  # Output: dict_keys(['AWB', 'Customer_name', 'Mobile', 'Address', 'PIN'])
    

    📦values( )

    This method in a dictionary is used to get a list of all the values in the dictionary.

     print(shipment1.values())
     # Output: dict_values([1001, 'Alex', 9999999999, 'Kadugodi, Bangalore, KA', 560067])
    

    📦items( )

    This method in a dictionary is used to get a list of all the key-value pairs in the dictionary.

     print(shipment1.items())
     # Output: dict_items([('AWB', 1001), ('Customer_name', 'Alex'), ('Mobile', 9999999999), ('Address', 'Kadugodi, Bangalore, KA'), ('PIN', 560067)])
    

    📦update()

    This method in a dictionary is used to add key-value pairs from another dictionary or a list of key-value pairs. If a key is already there, its value is changed to the new one.

     additional_info = {'Price': 500, 'Status': 'Shipped'}
     shipment1.update(additional_info)
     print(shipment1)
     # Output: {'AWB': 1001, 'Customer_name': 'Alex', 'Mobile': 9999999999, 'Address': 'Kadugodi, Bangalore, KA', 'PIN': 560067, 'Price': 500, 'Status': 'Shipped'}
    

    📦pop()

    This method in a dictionary removes a key-value pair by using the key. It returns the value of the removed key. If the key isn't found, it raises a KeyError.

     contact_number = shipment1.pop('Mobile')
     print(contact_number) # Output: 9999999999 print(shipment1)
    

    📦popitem()

    This method in a dictionary removes and returns the last key-value pair.

     print(shipment1.popitem()) # Output: ('PIN', 560067)
    

    📦clear()

    This method in a dictionary removes all key-value pairs, leaving the dictionary empty.

     shipment1.clear()
     print(shipment1)  # Output: {}
    

    Conclusion

    In this blog, we understand that Python's basic data structures—lists, tuples, sets, and dictionaries—can greatly improve how logistics operations work. By comparing these data structures to real-world logistics, we see that each one has unique benefits for handling data. Lists let you easily add and remove items, tuples keep data consistent because they can't be changed, sets ensure all items are unique, and dictionaries provide a clear way to store and access detailed data. Using these data structures can make logistics operations more efficient and dependable.

30
Subscribe to my newsletter

Read articles from Nabin Garai directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Nabin Garai
Nabin Garai