Difference between X.func and X.func( )
The syntax to access an attribute foo of an object or module is always .foo.
An attribute may or may not be callable.  An int is not callable (it's just a number), but a function is callable.  To call a function foo, you use parentheses, possibly with parameters inside, e.g. foo() or foo("bar").  Attempting to call something that is not callable will give you a TypeError.
So what syntax you use depends on whether the attribute is itself the value you want (e.g. an int or a str, or if it's a function that will return that value).  In your example, columns is itself an int, whereas count is a function that you need to call in order to get an int.
Note that it's possible in Python to wrap any value in a function, or to turn a function into a property (i.e. make an attribute that automatically calls a function to produce its value), but in general the convention is that if something requires some kind of dynamic computation it will be a function, and values that are predetermined will not require a function invocation to retrieve.
I've come across many such situations where I have used in built functions or modules where the syntax is sometimes "X.func_name" and other times (X.func_name()".
For example : 
In Pandas "df.columns" gives names of all columns and throws and throws error if written by mistake as "df.columns()"  #TypeError: 'Index' object is not callable.
Also in Pandas, "count()", "min()" etc are written as df.count() | df.min()
I hope I have explained my question properly.
I believe it has something to do with the OOP concept of Class and it's member functions but I'd like a more in-dept understanding.