Python does not, in fact, treat getFullName as an instance-level member differently from lastName and firstName in your example.  All of these are defined at the class-level an can be accessed as such.
If I change your example slightly:
class Person:
  lastName = ""
  firstName = ""
  def getFullName(self):
    return("{} {}".format(self.firstName, self.lastName))
Note that you can access all of these via the class:
>>> Person.firstName
''
>>> Person.getFullName
<function Person.getFullName at 0x104168c80>
And you can access all of these via an instance:
>>> p = Person()
>>> p.firstName
''
>>> p.getFullName
<bound method Person.getFullName of <__main__.Person object at 0x104173240>>
That is, they are all class attributes, and you can access any class attribute via an instance.
Now, you can see here that how you access the method getFullName does affect what you get, in that Person.getFullName is a function (that takes self as it's first argument) and p.getFullName is a bound method (in which self is already bound, so it doesn't take that argument):
>>> p = Person()
>>> Person.getFullName(p)
' '
>>> p.getFullName()
' '
Similarly, Person.firstName = "Bob" will set the class attribute firstName, whereas p.firstName = "Bob" will set the instance attribute firstName, and the instance variable will shadow the class variable next time you access p.firstName:
>>> Person.firstName = "Bob"
>>> p.firtName
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'Person' object has no attribute 'firtName'
>>> Person.firstName = "Bob"
>>> p.firstName 
'Bob'
>>> p.firstName = "Joe"
>>> p.firstName
'Joe'
>>> Person.firstName
'Bob'