Introduction

In this article, we will discuss object-oriented programming and the main differences between Python and C# OOP-wise. As a refresher, let’s give a quick read at Wikipedia’s definition of OOP:

Object-oriented programming (OOP) is a programming paradigm based on the concept of “objects”, which may contain data, in the form of fields, often known as attributes; and code, in the form of procedures, often known as methods.

Due to it’s strong OOP nature including encapsulation, inheritance, and polymorphism, there’s no discussion of C# being a mainly OOP language. However, and despite the fact that the remark “everything is an object” is basically a staple in any Python-related conversation covering OOP, the lack of an enforced encapsulation via accessors (public, private, etc…) and the loose definition of “object” may mislead learners (or even coders well versed into other languages) into thinking that Python is not a fully-fledged OOP language, which as we’ll see happens not to be true!

What is a Python Object?

Basically, an object is an abstraction of data. Any object in Python fulfills three requirements, having:

  1. An identity: An id representing the object’s address in memory. Never changes after an object is created.
  2. A type: the class that represents the attributes and methods an object has. It is also unchangeable.
  3. A value: sometimes it cannot change (immutable objects) and sometimes it can (mutable objects).

Knowing these basics for our objects, let’s get on with it and see how to create classes and objects.

Creating classes and Instancing objects

With Python’s classes being inspired by C++, the syntax will be familiar to C# developers as well. See these empty classes, for example:


class TestClass:
    pass
class TestClass {
}

There’s not much of interest that we can do with these objects, but it is enough for us to be able to instance said empty objects and take a look at their structure.

# Instancing an object of type TestClass directly
instance_one = TestClass()

# Getting the class type 
test_class = instance_one.__class__

# Instancing a TestClass object from the type directly
instance_two = test_class()

print(id(test_class))
print(id(instance_one))
print(id(instance_two))

In the snippet above we can see two different ways of instancing an object. The first one consist on simply adding a parenthesis at the end of our class name, which is very similar to C# var testClass = new TestClass();, yet no new keyword is needed.

After that, we have got the type using __class__. Just see the double underscore notation (or dunder) of this “magic object” that we’ll examine later. After getting the type, we can instance another object of the type by simple using the same () notation!

Moreover, this code will print something like this:

19383768
139943807271376
139943807271432

That’s because we have used Python’s built-in function id() to simply get an object’s id (as you can see, represented by integers). As expected, our two objects have different ids, as well as it does the representation of our type, as that is an object as well.

Class and Instance variables

No matter how clean our empty class is we can’t do much with it, so let’s transform it so that it has a few attributes and methods.

class TestClass:
    greeting = "Tooot!"
    loud_greeting = "TOOOOOOOOOOOOOOT!!!!"

    def greet(self):
        print(self.greeting)

    def greet_loudly(self):
        print(self.loud_greeting)

test_object = TestClass()
test_object.greet()
test_object.greet_loudly()

We have defined a couple of attributes in the class, and then two methods that simply print the value of the attributes. As you would expect, the code above prints this:

Tooot!
TOOOOOOOOOOOOOOT!!!!

However, this simple code still have several subtleties to stop and watch closely. We know that we have defined the attributes and methods in the class and that they seem to work perfectly from an instance, but who “owns” the attributes? Do they belong to the class? Or rather they are the instance’s? What about the methods?

Did you notice that self parameter in the method? And how we didn’t need to pass the argument when we called the method? In short, Python handles that for you. For the sake of optimisation, all Python objects belonging to a class reuse the same method defined in the class, instead of sitting in every instance of said type. In exchange, Python needs to know who is the instance calling, although fortunately that is done pretty much automatically in most scenarios.

But still we might not be 100% sure of the owner of our attributes, so let’s make some more changes to clarify. Besides having some greeting methods, we’ll want to know as well the count of instances that we have created of that given class. It will look like this now, and you’ll find a couple of surprises in there:

class TestClass:
    instance_count = 0
    greeting = "Tooot!"
    loud_greeting = "TOOOOOOOOOOOOOOT!!!!"

    def __init__(self): 
      TestClass.instance_count += 1

    def greet(self):
        print(self.greeting)

    def greet_loudly(self):
        print(self.loud_greeting)

test_object = TestClass()
another_test_object = TestClass()

test_object.greet()
another_test_object.greet_loudly()

print(TestClass.instance_count)

Note how we have defined another method called __init__, another one of these “magic attributes” with a double underscore. That is the object initialiser, with might be seen as some sort of “constructor”. We have added a statement in our init, and it is no less than a “plus one” to a variable of the class, that is named and accessed directly. If we run that code, we’ll find this:

Tooot!
TOOOOOOOOOOOOOOT!!!!
2

So that’s our mystery solved! all instance_count, greeting, and loud_greeting are class attributes. On any single object instantiation, the counter of the class attribute is increased automatically. We’ve instanced to objects, hence increased the counter by two. Let’s keep making our code better: now we’ll pass a custom greeting on the object’s initialisation and set it as an instance attribute, but we’ll also let the objects use the default class greeting in case we don’t provide a custom greeting. Any class attribute defined in the class scope (such as instance_count in our example) can be thought of as some sort of static/class-wide variable.

class TestClass:
    instance_count = 0
    default_greeting = "Tooot!"

    def __init__(self, greeting = None): 
        if greeting is not None:
            self.greeting = greeting
        TestClass.instance_count += 1

    def greet(self):
        try:
            print(self.greeting)
        except AttributeError:
            print(TestClass.default_greeting)

test_object = TestClass()
custom_test_object = TestClass("Hello mate!")

test_object.greet()
custom_test_object.greet()
Tooot!
Hello mate!

This is a bit better, as it removes ambiguity on whether our attributes live in the class or in the instance. Also note that if we have a class and an instance attribute with the same name and we do access it, the instance variable will be the one which will be attempted first, then the class one, so it’s probably a good idea to name class and instance variables differently to avoid misunderstandings and unexpected references. Also, see how we are passing the greeting as an optional parameter to the initialiser, and also printing either greeting in the method depending on availability (with the instance being the preferred options, falling back to class in case of exception). So that’s our attributes sorted out. What about the methods?

A first option would be to decorate our method with @staticmethod. In this fashion, the method becomes static and directly callable from the class itself. Another option would be to decorate with @classmethod, causing the same behaviour, but requiring the method to be declared with a mandatory argument, normally called cls, or the class that is calling the method. As with self, it is passed automatically, as demonstrated below.

class TestClass:
    default_greeting = "Tooot!"

    @classmethod
    def class_greet(cls):
        print(cls.default_greeting)
    
    @staticmethod
    def static_greet():
        print(TestClass.default_greeting)

TestClass.class_greet()
TestClass.static_greet()
Tooot!
Tooot!

Seeing Methods, Attributes and Magic attributes

Now that we’re a bit better equipped we can get back to that “everything is an object” mantra. We can very easily inspect our class and get information about all of its properties straight away, easily and without a significant performance overhead via introspection (unlike we normally experience with reflection). Let’s take our last static class and inspect it with built-in method dir():

class TestClass:
    default_greeting = "Tooot!"

    @classmethod
    def class_greet(cls):
        print(cls.default_greeting)
    
    @staticmethod
    def static_greet():
        print(TestClass.default_greeting)

print(dir(TestClass))
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__',
'__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', 
'__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', 
'__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__','__weakref__', 
'class_greet', 'default_greeting', 'static_greet']

There! Behold many of the magic attributes, standard attributes, and methods of the class. Introspection is one of Python biggest strengths, but that shall be covered separately in a different article, as we probably will with the very powerful multiple inheritance. Thanks for reading!

Additional Resources