Your friend was wrong. Methods are attributes.
Everything in Python is objects, really, with methods and functions and anything with a
__call__() method being callable objects. They are all objects that respond to the
() call expression syntax.
Attributes then, are objects found by attribute lookup on other objects. It doesn't matter to the attribute lookup mechanism that what is being looked up is a callable object or not.
You can observe this behaviour by looking up just the method, and not calling it:
>>> class Foo(object):
... def bar(self):
>>> f = Foo()
<bound method Foo.bar of <__main__.Foo object at 0x1023f5590>>
f.bar is an attribute lookup expression, the result is a method object.
Of course, your friend me be a little bit right too; what is really happening is that methods are special objects that wrap functions, keeping references to the underlying instance and original function, and are created on the fly by accessing the function as an attribute. But that may be complicating matters a little when you first start out trying to understand Python objects. If you are interested in how all that works, read the Python Descriptor HOWTO to see how attributes with special methods are treated differently on certain types of attribute access.
This all works because Python is a dynamic language; no type declarations are required. As far as the language is concerned, it doesn't matter if
Foo.bar is a string, a dictionary, a generator, or a method. This is in contrast to compiled languages, where data is very separate from methods, and each field on an instance needs to be pinned down to a specific type beforehand. Methods generally are not objects, thus a separate concept from data.