Python Hash Table implemented using lists
Restructuring and micro-optimization

Hard-coded table size 54 is better defined as class constant:

T_SIZE = 54


Encapsulate the crucial list/table at least as protected property self._Table.

Calling self._get_value(key) and the condition if self._Table[val] == None are repeated in most crucial methods. To reduce that noisy repetition an additional method can be defined which will return a tuple of calculated value val and is_empty ("empty slot" flag):

def _check_value(self, key):
    val = self._get_value(key)
    is_empty = self._Table[val] is None
    return val, is_empty


It doesn't make sense to construct if ... else conditional if the 1st if branch return 's immediately.

Both delete and lookup methods, on existing item, will perform 2 access/lookup operations on self._Table[val]:


key in self._Table[val]
self._Table[val].index(key) 


To reduce access operations there we can apply try/except trick and returning the needed result in each separate block. See the final implementation below:

class HashTable:
    T_SIZE = 54

    # Initialize the table with a fixed size
    def __init__(self):
        self._Table = [None] * HashTable.T_SIZE

    def _get_value(self, key):
        total = hash(key)
        return total % HashTable.T_SIZE

    def _check_value(self, key):
        val = self._get_value(key)
        is_empty = self._Table[val] is None
        return val, is_empty

    def insert(self, key):
        val, is_empty = self._check_value(key)
        col = False  # Collision flag
        index = 0

        if is_empty:  # Empty slot - turn into list of keys to avoid extra cases
            self._Table[val] = [key]
        else:
            self._Table[val].append(key)  # Collision - append
            col = True
            index = len(self._Table[val]) - 1

        return val, col, index

    def delete(self, key):
        val, is_empty = self._check_value(key)
        if is_empty:  # Deleting unexisting element
            return -1, 0

        try:
            index = self._Table[val].index(key)
            self._Table[val].remove(key)
            return val, index
        except ValueError:  # No match was found in list, element does not exist
            return -1, 0

    def lookup(self, key):
        val, is_empty = self._check_value(key)
        if is_empty:
            return -1, 0
        try:
            index = self._Table[val].index(key)
            return val, index
        except ValueError:  # No match was found in list, element does not exist
            return -1, 0

    def clear(self):
        self.__init__()


In case if _get_value method won't be used in standalone context - you may easily inline it into _check_value method.

Sample usage:

h = HashTable()
h.insert('abc')
h.insert('abc')
print(h.lookup('abc'))
print(h.lookup('1abc'))
print(h.delete('abc'))
print(h.delete('abc'))
print(h.delete('abc'))


The output (consecutively):

(8, 0)
(-1, 0)
(8, 0)
(8, 0)
(-1, 0)