Object Oriented Programming (OOP) in Python

Mert Barbaros
Apr 3 · 16 min read

1. Objects in Python

Everything in python is an object

a = 12 
b = 4
print(a + b)
print(a.__add__(b))

add method and + comes from the same method. OOP uses classes and methods that encapsulates both data and the functions that operate on that data. In Python every object is a type. This is why we can use the add method for a summation (it’s part of a class) If a function is a part of a class, we will call it method. Classes are boilerplates which objects can be created so when we create objects of this kettle class. Now we have a name and price of the kettle and each instance of the class will have its own values for name and price. So the classes are the templates that all the objects created and objects from the same class will share the same characteristics. So the instance is just another name for an object created from a class definition.

class Kettle(object):

def __init__(self, make, price):
self.make = make
self.price = price
self.on = False

#first kettle
kenwood = Kettle("Kenwood", 8.99)
print(kenwood.make)
print(kenwood.price)

#adjust the price
kenwood.price = 12.99
print(kenwood.price)

#second kettle

hamilton = Kettle("Hamilton", 14.99)
print(hamilton.make)
print(hamilton.price)

#printing models and their prices
print("Models {} = {}, {} = {}".format(kenwood.make, kenwood.price, hamilton.make, hamilton.price))

If we create a kettle called Kenwood, it is an object of a type kettle. In this example we created couple of kettle objects.

We can access to values of instance via . notation like in the kettle example (kenwood.price, kenwood.make).

Output:

Kenwood 
8.99
12.99
Hamilton
14.99
Models Kenwood = 12.99,
Hamilton = 14.99
Process finished with exit code 0

2. Instances, Constructors & Self

In python, when the variable is bound the instance of a class, we call it as data attribute (data members in C++, fields in Java). Since the Kenwood and Hamilton are objects, we can specify their attributes in the replacement fields as

print("Models {0.make} = {0.price}, {1.make} = {1.price}".format(kenwood, hamilton)

When we run the kettle.py again, we will end up with the same result.

Kenwood 8.99 
12.99
Hamilton
14.99
Models Kenwood = 12.99,
Hamilton = 14.99
Models Kenwood = 12.99,
Hamilton = 14.99
Process finished with exit code 0

Since now, we learned some basics about object oriented programming in python.

Class: template for creating object. All objects created using the same class will have the same characteristics.

Object: An instance of a class.

Instantiate: Create an instance of a class.

Method: A function defined in a class.

Attribute: A variable bound to an instance of a class.

Before we jump into the constructors, let’s improve our kettle class with additional methods.

class Kettle(object):

def __init__(self, make, price):
self.make = make
self.price = price
self.on = False

def switch_on(self):
self.on = True

#switch on kenwood
print(kenwood.on)
kenwood.switch_on()
print(kenwood.on)

switch.on method will change self.on to True. In that point, we can talk about self parameter. In reality, we can write anything instead of self. However, in general we use self as a parameter name. Self parameter says “that object”. In this case, when we invoke the kenwood.switch_on(), self parameter helps us to switch that kettle which is kenwood. So the self is the reference to the instance of the class.

Output

False
True

Process finished with exit code 0

We could also write the same invoking code like this

#same invoking
Kettle.switch_on(hamilton)
print("Hamilton is on: {}".format(hamilton.on))

Output

Hamilton is on: True

In this example, we did not use an instance of kettle to call a method, we have use the class itself because there is no instance here the value for self has to be specified and failing to do so would give an error.

We can create a instance variable (data attribute) on outside of the class. Let’s create a power instance variable with

create a new instance variable

kenwood.power = 1.5
print("Kenwood power is:", kenwood.power)
print("Hamilton power is", hamilton.power)

Output

Kenwood power is: 1.5

Traceback (most recent call last):
File "/Users/mertbarbaros/Documents/MEF University/OOP/OOP Learning/kattle.py", line 44, in <module>
print("Hamilton power is", hamilton.power)
AttributeError: 'Kettle' object has no attribute 'power'

As you can see, we clearly defined the power attribute for Kenwood, but we got error message for hamilton that said “Kettle” object has no attribute “power”. That’s because we didn’t create one. In this example we clearly saw that, instances created from the same class template but which ultimately have different attributes. It can be useful feature but also cause problems if you make a typing error when trying to assign a value to an existing data attribute. Instead of that, we can use subclassing which is where a new class is created from an existing class. We will analyze the subclasses in the later of this post.

Before we finish this section, we should remind that methods are also attribute of a class. This is why we will keep using term instance variable instead of data attribute.

2.1 Class Attributes

In the kettle class, make and price is belong to each instance but also possible for the class to have attributes which effects to all instances. Let’s improve our Kettle class with power source class attribute.

class Kettle(object):

power_source = "Electricity"

def __init__(self, make, price):
self.make = make
self.price = price
self.on = False

def switch_on(self):
self.on = True

#Class Attribute Invoking
print(Kettle.power_source)
print(kenwood.power_source)
print(hamilton.power_source)

power_source is a class attribute. What we have done is, all instances of Kettle class will share the same power source.

Output

Electricity
Electricity
Electricity

Let’s examine the namespaces of the three objects to verify the two instances are sharing the same attribute which only exists in the class and we access the namespace via the dic attribute:

#Namespace
print("Kettle",Kettle.__dict__)
print("Kenwood",kenwood.__dict__)
print("Hamilton",hamilton.__dict__)

Output

Kettle {'__module__': '__main__', 'power_source': 'Electricity', '__init__': <function Kettle.__init__ at 0x7fe156803670>, 'switch_on': <function Kettle.switch_on at 0x7fe156803700>, '__dict__': <attribute '__dict__' of 'Kettle' objects>, '__weakref__': <attribute '__weakref__' of 'Kettle' objects>, '__doc__': None}
Kenwood {'make': 'Kenwood', 'price': 12.99, 'on': True, 'power': 1.5}
Hamilton {'make': 'Hamilton', 'price': 14.99, 'on': True}

As you can see from the output, Kettle class have many attributes like power_source, init method, switch_on method and so on. But the instances (Kenwood and Hamilton) has only instance variables (make and price). When we try to access power_source from the instance, Python checks the variable in the instance name space, if it doesn’t which is the case here, then checks the class for the instance and finds power source in the Kettle class.

We can also change the value of class attribute like we did in instance variables.

#change the class attribute
Kettle.power_source = "atomic"
print("New power source is:", Kettle.power_source)

Output

New power source is: atomic

However, we can change the value of class attribute in instance variable level. Let’s change the power source of Kenwood

#change kenwood class attribute
kenwood.power_source = "gas"
print("Kenwood power source", kenwood.power_source)
print("Hamilton power source", hamilton.power_source)

Output

Kenwood power source gas
Hamilton power source atomic

Here is the full code of our Kettle example with output

class Kettle(object):    #class attributes
power_source = "Electricity"
def __init__(self, make, price):
self.make = make
self.price = price
self.on = False
def switch_on(self):
self.on = True
#first kettle
kenwood = Kettle("Kenwood", 8.99)
print(kenwood.make)
print(kenwood.price)
#adjust the price
kenwood.price = 12.99
print(kenwood.price)
#second kettlehamilton = Kettle("Hamilton", 14.99)
print(hamilton.make)
print(hamilton.price)
#printing models and their prices
print("Models {} = {}, {} = {}".format(kenwood.make, kenwood.price, hamilton.make, hamilton.price))
print("Models {0.make} = {0.price}, {1.make} = {1.price}".format(kenwood, hamilton))
#switch on kenwood
print(kenwood.on)
kenwood.switch_on()
print(kenwood.on)
#same invoking
Kettle.switch_on(hamilton)
print("Hamilton is on: {}".format(hamilton.on))
#create a new instance variablekenwood.power = 1.5
print("Kenwood power is:", kenwood.power)
#Class Attribute Invoking
print(Kettle.power_source)
print(kenwood.power_source)
print(hamilton.power_source)
#Namespace
print("Kettle",Kettle.__dict__)
print("Kenwood",kenwood.__dict__)
print("Hamilton",hamilton.__dict__)
#change the class attribute
Kettle.power_source = "atomic"
print("New power source is:", Kettle.power_source)
#change kenwood class attribute
kenwood.power_source = "gas"
print("Kenwood power source", kenwood.power_source)
print("Hamilton power source", hamilton.power_source)

Output:

Kenwood
8.99
12.99
Hamilton
14.99
Models Kenwood = 12.99, Hamilton = 14.99
Models Kenwood = 12.99, Hamilton = 14.99
False
True
Hamilton is on: True
Kenwood power is: 1.5
Electricity
Electricity
Electricity
Kettle {'__module__': '__main__', 'power_source': 'Electricity', '__init__': <function Kettle.__init__ at 0x7fa0d6103550>, 'switch_on': <function Kettle.switch_on at 0x7fa0d61035e0>, '__dict__': <attribute '__dict__' of 'Kettle' objects>, '__weakref__': <attribute '__weakref__' of 'Kettle' objects>, '__doc__': None}
Kenwood {'make': 'Kenwood', 'price': 12.99, 'on': True, 'power': 1.5}
Hamilton {'make': 'Hamilton', 'price': 14.99, 'on': True}
New power source is: atomic
Kenwood power source gas
Hamilton power source atomic
Process finished with exit code 0

2.2. Methods

Python encapsulates data and objects using either an object oriented paradigm or a modular approach. We can see the modular approach when using the pytz module to handle time zone. You don’t have to know how pytz was implemented in order to use it. All information encapsulated within the pytz module.

Let’s create an another example about bank accounts. Initially, this class will show the current balance, make deposit or withdraw transactions. Here is the initial code.

class Account:
""" Simple bank account with balance """
def __init__(self, name, balance):
self.name = name
self.balance = balance
print("Account created for", self.name)
def deposit(self, amount):
if amount > 0:
self.balance += amount
def withdraw(self, amount):
if amount > 0:
self.balance -= amount
def show_balance(self):
print("Balance is", self.balance)

In this code we have a new item called docstring (“”” Simple bank account with balance “””). It’s a very good idea to define the class in the beginning of the class. Also let’s talk about init method. In earlier, we called it constructor but it’s not entirely true. There is 2 methods for creating a class in python. First method to be called when a class instance is created is new and takes care of the actual creation. The init method then customizes the instance performing tasks such as giving values to the data attributes. Technically, the class constructor is the new method which is actually calling (_ _ new_ _) as you probably expect. Generally speaking you don’t need to define new except in special cases when subclassing certain types of classes.

class Account:
""" Simple bank account with balance """
def __init__(self, name, balance):
self.name = name
self.balance = balance
print("Account created for", self.name)
def deposit(self, amount):
if amount > 0:
self.balance += amount
self.show_balance()
def withdraw(self, amount):
if 0 < amount <= self.balance:
self.balance -= amount
else:
print("Amount must be greater then zero and more than your account balance")
def show_balance(self):
print("Balance is", self.balance)
if __name__ == '__main__':
mert = Account('Mert', 0)
mert.show_balance()
mert.deposit(500)
mert.show_balance()
mert.withdraw(100)
mert.show_balance()
mert.withdraw(50000)

Output

Account created for Mert
Balance is 0
Balance is 500
Balance is 500
Balance is 400
Amount must be greater then zero and more than your account balance
Process finished with exit code 0

if name == ‘main’: açıkla

transaction logs

import datetime
import pytz
class Account:
""" Simple bank account with balance """
def __init__(self, name, balance):
self.name = name
self.balance = balance
self.transaction_list = []
print("Account created for", self.name)
def deposit(self, amount):
if amount > 0:
self.balance += amount
self.show_balance()
#store the transaction with datetime
self.transaction_list.append((pytz.UTC.localize(datetime.datetime.utcnow()), amount))
def withdraw(self, amount):
if 0 < amount <= self.balance:
self.balance -= amount
else:
print("Amount must be greater then zero and more than your account balance")
def show_balance(self):
print("Balance is", self.balance)
def show_transactions(self):
for date, amount in self.transaction_list:
if amount > 0:
transaction_type = 'deposited'
else:
transaction_type = 'withdrawn'
amount *= -1
print("{:6} {} on {} ( local time was {} )".format(amount, transaction_type, date, date.astimezone()))
if __name__ == '__main__':
mert = Account('Mert', 0)
mert.show_balance()
mert.deposit(500)
mert.show_balance()
mert.withdraw(100)
mert.show_balance()
mert.withdraw(50000)

2.2.1 Static Methods

Let’s change our Account class little bit:

class Account:
""" Simple bank account with balance """
@staticmethod
def _current_time():
utc_time = datetime.datetime.utcnow()
return pytz.utc.localize(utc_time)
def __init__(self, name, balance):
self.name = name
self.balance = balance
self.transaction_list = []
print("Account created for", self.name)

First change is we added current_time method above the init method. We started the method with underscore. Static method is shared by all instances of the class in the same way that the power_source class attribute of our kettle class was shared by all instances. It’s very easy to create. Just put @staticmethod above the method. About the underscore, the convention is that names starting with an underscore are non-public. The account class is concerned with managing bank accounts not with dates and times so although clients can call the current underscore time method if they want to, the underscore makes it clear that this method isn’t intended to be used outside of the class. If someone use the current time method outside of the class, they run the risk that we may remove it in later version or change to something completely different in the future.

Now we add our static method, we can go back to our deposit method and change it :

def deposit(self, amount):
if amount > 0:
self.balance += amount
self.show_balance()
#store the transaction with datetime
self.transaction_list.append((Account._current_time(), amount))

Notice that, we called our static method using class name like Account._current_time(). That’s because current time method is static or class method. We could also use self._current_time() and the code would still works but performance would slightly different. Because Python would look for the method in instance namespace first then in the class namespace.

Let’s create a new account and make some transaction

if __name__ == '__main__':
# mert = Account('Mert', 0)
# mert.deposit(100000)
# mert.show_balance()
# mert.withdraw(50000)
# mert.deposit(12000)
# mert.show_balance()
# mert.show_transactions()
yaren = Account('Yaren', 800)
yaren.deposit(100)
yaren.withdraw(200)
yaren.show_balance()
yaren.show_transactions()

Output

Account created for Yaren
Balance is 900
Balance is 700
100 deposited on 2020-12-16 22:18:30.277889+00:00 ( local time was 2020-12-17 01:18:30.277889+03:00 )
200 withdrawn on 2020-12-16 22:18:30.277899+00:00 ( local time was 2020-12-17 01:18:30.277899+03:00 )

One problem is about our Account class is client can easily modify the balance and transaction history wouldn’t effect. So we should limit the client. So we can change the name in the init method and all attributes can start with underscore then. I used the Refactor feature on PyCharm to change all names.

def __init__(self, name, balance):
self._name = name
self._balance = balance
self._transaction_list = [(Account._current_time(), balance)]
print("Account created for", self._name)

The rule is, variables start with underscore is internal use only. You can mess with them but you probably break something. It’s non public, not private. Because private implies that it’s enforced. When the name start with two underscore, that is really intended for use when subclassing.

Here is the final code of Account class

import datetime
import pytz

class Account:
""" Simple bank account with balance """

@staticmethod
def _current_time():
utc_time = datetime.datetime.utcnow()
return pytz.utc.localize(utc_time)

def __init__(self, name, balance):
self._name = name
self._balance = balance
self._transaction_list = [(Account._current_time(), balance)]
print("Account created for", self._name)

def deposit(self, amount):
if amount > 0:
self._balance += amount
self.show_balance()
#store the transaction with datetime
self._transaction_list.append((Account._current_time(), amount))

def withdraw(self, amount):
if 0 < amount <= self._balance:
self._balance -= amount
self._transaction_list.append((Account._current_time(), -amount))
else:
print("Amount must be greater then zero and more than your account balance")

def show_balance(self):
print("Balance is", self._balance)

def show_transactions(self):
for date, amount in self._transaction_list:
if amount > 0:
transaction_type = 'deposited'
else:
transaction_type = 'withdrawn'
amount *= -1
print("{:6} {} on {} ( local time was {} )".format(amount, transaction_type, date, date.astimezone()))

if __name__ == '__main__':
# mert = Account('Mert', 0)
# mert.deposit(100000)
# mert.show_balance()
# mert.withdraw(50000)
# mert.deposit(12000)
# mert.show_balance()
# mert.show_transactions()

yaren = Account('Yaren', 800)
yaren.deposit(100)
yaren.withdraw(200)
yaren.show_balance()
yaren.show_transactions()

2.2.2. DocStrings and Song Example

In this section we will start a new example and create our DocString. We don’t need to explain everything on that class, because DocString will do.

class Song:
""" Class to represent a song
Attributes:
title(str): Title of the song
artist(Artist): An artist object represents the songs creater
duration (int): Duration of the song in seconds and can be zero
"""
def __init__(self, title, artist, duration = 0):
""" Song init method
Args:
title(str): Initilises the title attribute
artist(Artist): At artist object represents the song's creater
duration(int): Initial value for duration attribute, will default zero.
"""
self.title = title
self.artist = artist
self.duration = duration

Inheritance

Think about birds, they have all different features but there are common things that make them birds. So, we can define a base class that objects are based on, things that are common for classes that derive from the base class. Then we can allow a class to define the unique characteristics of itself.

So all our birds inherit some basic properties from their base class, which is often referred to as the Super Class. They also inherit methods, so all our birds can walk and eat, but the individual bird classes also their own, unique, properties and methods. Penguins can swim and Ostriches can run, for example.

The individual bird classes are referred to as Sub Classes. So Crow is a Sub Class of Bird — which makes Bird a Super Class of Crow. The idea of sub-classing can be taken further, so we could add subclasses of bird such as FlyingBird and FlightlessBird.

In python, classes can be inherited from multiple classes which is called multiple inheritance. So why inheritance?

Allows you to write code once, then for it to be used automatically by other classes. If we created Eagle object, it would automatically get Beak and Wings properties. Let’s get deep on inheritance but first revised the getters and setters.

Getters and Setters

We don’t need to define getters and setters for changing the values in Python but they are still useful. We will work on a game project in this section and we will separate the python files in the same directory.

First class will show us the players that play the game (player.py). Also I will put all the code to the main file.

Let’s initiate our player class

class Player(object):

def __init__(self, name):
self.name = name
self.lives = 3
self.level = 1
self.score = 0

And create our first player in the main.py file

import player

tim = player.Player("Tim")
print(tim.name)
print(tim.lives)
tim.lives -= 1
print(tim.lives)

Output

Tim
3
2

Process finished with exit code 0

So, let’s start to define getters and setters. Getters are used to get the value of data attribute. We could use print(tim.get_name()) instead of print(tim.name). We call that method getter. We don’t generally don't have to use getters. Setters are created for set the value of the attribute. tim.set_lives(300). Setters would be handy for updating the attribute.

class Player(object):    def __init__(self, name):
self.name = name
self._lives = 3
self.level = 1
self.score = 0
def _get_lives(self):
return self._lives
def _set_lives(self, lives):
if lives >= 0:
self._lives = lives
else:
print("Life can not be negative")
self._lives = 0
lives = property(_get_lives, _set_lives)

When we printing an object, python use special method str like in the above method. We will call that method when we need the string representation of an object. First let’s test this on main.

main.py

import playertim = player.Player("Tim")
print(tim.name)
print(tim.lives)
tim.lives = -1
print(tim.lives)
tim.lives = -1
print(tim.lives)
tim.lives = -1
print(tim.lives)
tim.lives = -1
print(tim.lives)
tim.lives = -1
print(tim.lives)

Output

Tim
3
Life can not be negative
0
Life can not be negative
0
Life can not be negative
0
Life can not be negative
0
Life can not be negative
0
Process finished with exit code 0

Because we created str function, actually we can print out Tim.

import playertim = player.Player("Tim")
print(tim)
print(tim)
tim.lives = -1
print(tim)
tim.lives = -1
print(tim)
tim.lives = -1
print(tim)
tim.lives = -1
print(tim)
tim.lives = -1
print(tim)

_lives firstly hide the attribute from the client and the property and attribute name differs. Also we didn’t call the method while we creating the property

lives = property(_get_lives, _set_lives)

instead we use the name of the methods, because we don’t want to call them, we want to use them. Also we can use self as a replacement field in classes.

Let’s improve the classes. Let’s modify the player class so the players’ scores are increased by one thousand every time their level increases by one. If player drop back a level, they’ll lose on thousand for each level they drop back. We will create a property called level and create the getters and setters for changing the score.

Here is the code for player

class Player(object):    def __init__(self, name):
self.name = name
self._lives = 3
self._level = 1
self.score = 0
def _get_lives(self):
return self._lives
def _set_lives(self, lives):
if lives >= 0:
self._lives = lives
else:
print("Life can not be negative")
self._lives = 0
def _get_level(self):
return self._level
def _set_level(self, level):
if level > 0 :
delta = level - self._level
self.score += delta * 1000
self._level = level
else:
print("Level can't be less then 1")
lives = property(_get_lives, _set_lives)
level = property(_get_level, _set_level)
def __str__(self):
return "Name: {0.name}, Lives: {0.lives}, Level: {0.level}, Score: {0.score}".format(self)

and main

import playertim = player.Player("Tim")
tim._lives = 9
print(tim)
tim.level = 2
print(tim)
tim.level += 5
print(tim)

Output

Name: Tim, Lives: 9, Level: 1, Score: 0
Name: Tim, Lives: 9, Level: 2, Score: 1000
Name: Tim, Lives: 9, Level: 7, Score: 6000
Process finished with exit code 0

Let’s look at the alternative syntax that adding a property and updating the score called decorators. First we will hide the score variable in init. Then create getters and setters for score with decorator.

class Player(object):    def __init__(self, name):
self.name = name
self._lives = 3
self._level = 1
self._score = 0
def _get_lives(self):
return self._lives
def _set_lives(self, lives):
if lives >= 0:
self._lives = lives
else:
print("Life can not be negative")
self._lives = 0
def _get_level(self):
return self._level
def _set_level(self, level):
if level > 0 :
delta = level - self._level
self._score += delta * 1000
self._level = level
else:
print("Level can't be less then 1")
lives = property(_get_lives, _set_lives)
level = property(_get_level, _set_level)
#that is the getter
@property
def score(self):
return self._score
#that is the setter
@score.setter
def score(self, score):
self._score = score
def __str__(self):
return "Name: {0.name}, Lives: {0.lives}, Level: {0.level}, Score: {0.score}".format(self)

Back In Inheritance

Let’s go back to inheritance. We will create enemy super class.

cclass Enemy:    def __init__(self, name='Enemy', hit_points=0, lives=1):
self.name = name
self.hit_points = hit_points
self.lives = lives
def take_damage(self, damage):
remaining_points = self.hit_points - damage
if remaining_points >= 0:
self.hit_points = remaining_points
print("I took {} points damaged and have {} left".format(damage, self.hit_points))
else:
self.lives -= 1
#print attributes
def __str__(self):
return "Name: {0.name}, Lives: {0.lives}, Hit Points: {0.hit_points}".format(self)

This is a super class. Any class inherited from this class will have name, hit points and lives.

main

import playertim = player.Player("Tim")from enemy import Enemyrandom_monster = Enemy("Basic Enemy", 12, 1)
print(random_monster)
random_monster.take_damage(4)
print(random_monster)

Output

Name: Basic Enemy, Lives: 1, Hit Points: 12
I took 4 points damaged and have 8 left
Name: Basic Enemy, Lives: 1, Hit Points: 8
Process finished with exit code 0

Now, let’s create our subclasses. Our first monster will troll.

Because we inherited from Enemy we started with Troll(Enemy).

class Troll(Enemy):
pass

main

import playertim = player.Player("Tim")from enemy import Enemy, Trollugly_troll= Troll()
print("ugly troll: {}".format(ugly_troll))
another_troll = Troll("Ug", 18, 1)
print("another troll: {}".format(another_troll))

Output

ugly troll: Name: Enemy, Lives: 1, Hit Points: 0
another troll: Name: Ug, Lives: 1, Hit Points: 18
Process finished with exit code 0

Calling Super Methods

Let’s make our subclasses more useful. Now, we will call the init method of the superclass Enemy inside our trolls init method.

class Enemy:    def __init__(self, name='Enemy', hit_points=0, lives=1):
self.name = name
self.hit_points = hit_points
self.lives = lives
def take_damage(self, damage):
remaining_points = self.hit_points - damage
if remaining_points >= 0:
self.hit_points = remaining_points
print("I took {} points damaged and have {} left".format(damage, self.hit_points))
else:
self.lives -= 1
#print attributes
def __str__(self):
return "Name: {0.name}, Lives: {0.lives}, Hit Points: {0.hit_points}".format(self)
class Troll(Enemy): def __init__(self, name):
#pyton 2: Enemy.__init__(self, name=name, lives=1, hit_points=23)
#python 3
#super(Troll, self).__init__(name=name, lives=1, hit_points=23)
super().__init__(name=name, lives=1, hit_points=23)

Main

import playertim = player.Player("Tim")from enemy import Enemy, Trollugly_troll= Troll("Pug")
print("ugly troll: {}".format(ugly_troll))
another_troll = Troll("Ug")
print("another troll: {}".format(another_troll))
brother= Troll("Og")
print(brother)

Output

ugly troll: Name: Pug, Lives: 1, Hit Points: 23
another troll: Name: Ug, Lives: 1, Hit Points: 23
Name: Og, Lives: 1, Hit Points: 23
Process finished with exit code 0

Subclass can have different methods and attributes. Let’s demonstrate that.

def grunt(self):
print("Me {0.name} to stop you".format(self))

Also we can change the behavior of the superclass.

Star Gazers

“If you want to master something, teach it.”

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store