My own expandable list in Python
Just some random things I noticed while looking over your code:



The way you're handling additions is a little odd to me. You seem to be continually maintaining an "empty node" at the end of the list, then when you add, you iterate to that node, replace its content then give it a new, empty node. Why not just have nextNode remain None, then iterate while currentNode.hasNextNode() to find the last node? Something like:

def getLastNode(self):
    currentNode = self.startNode
    while currentNode.hasNextNode():
        currentNode = currentNode.nextNode
    return currentNode

def insert(self, obj):
    newNode = Node()
    newNode.object = obj

    lastNode = self.getLastNode()
    lastNode.nextNode = newNode


From my experience, this is a far more typical set up. This also has the miniscule benefit of using slightly less memory, since you're no longer keeping one more node than needed in memory at all times. This also does away with the need for the hasObject method.



The definition of getNumberOfElements is completely impractical for most applications. You can assume for most, if not all data structures (besides maybe lazy lists), that a size method will run in O(1) time. Iterating the entire list every time you want to get the size has the opportunity to bring your application to a crawl; especially if the user of the class isn't aware that getNumberOfElements is O(n).

Just give your class a n_nodes field, and increment/decrement it in the appropriate methods:

def insert(self, obj):
    newNode = Node()
    newNode.object = obj

    lastNode = self.getLastNode()
    lastNode.nextNode = newNode

    self.n_nodes += 1 # Here


Then change getNumberOfElements to something like:

def size(self):
    return self.n_nodes


You can now get the size of the list nearly instantly instead of requiring a full iteration. The only drawback is you need to take care to properly manage n_nodes in all the methods that can change the size of the list.



Python isn't my main language, but I'm pretty sure snake_case is idiomatic Python. Coming from Java, you're probably more used to camelCase, but adhering to conventions is important for allowing other people to read your code easier. One thing you need to adjust to when writing in multiple languages is remembering what conventions the language you're writing uses. Clojure, my main language, uses dash-case; but this isn't even valid in many languages (like Python). I've gotten yelled at many times on Stack Overflow when answering questions in languages I don't use often because I forgot to adjust my naming conventions, and end up using either unidiomatic, or outright illegal names.



It would likely simplify your life if you gave the Node constructor the ability to directly set nextNode and object fields:

def __init__(self, newObject = None, newNextNode = None):
    self.object = newObject
    self.nextNode = newNextNode


That will save you a couple lines in the few places by just directly passing the object:

def insert(self, obj):
    newNode = Node(obj)

    # No longer needed!
    # newNode.object = obj

    lastNode = self.getLastNode()
    lastNode.nextNode = newNode

    self.n_nodes += 1




The "start node" of a linked list is more typically referred to as the "root".



def delete(self, index):
    if index == 0:
        startNode = self.getNodeByIndex(1)
...


Could probably be written more cleanly as simply

def delete(self, index):
    if index == 0:
        startNode = startNode.nextNode
...




For efficiency, you might also want to maintain a lastNode field. Right now, to get to the last node, you're needing to iterate the entire list, which is quite expensive. If you just keep a reference to the last node when you do an insertion, you can just use that reference instead of needing to iterate.



if index == (self.getNumberOfElements) - 1:


Is broken. You need to call the method.