COMP 364: Object Oriented Programming (OOP) Pt. I

FYI From Last Lecture

If you want to try the Twitter example, here is how to do the authentication.

  1. Register here
  2. Go to “Keys and Access Tokens” tab, and copy your “API key” and “API secret”. Scroll down and click “Create my access token”, and copy your “Access token” and “Access token secret”.
  3. pip install requests_oauthlib
  4. Include this in your code:
>>> import requests
>>> from requests_oauthlib import OAuth1

>>> url = ''
#replace placeholders with your keys from registering with Twitter
>>> auth = OAuth1('YOUR_APP_KEY', 'YOUR_APP_SECRET',

>>> requests.get(url, auth=auth)
<Response [200]>

What is OOP?

Let us turn to the Bible.

"And God made the beast of the earth after his kind, and cattle after their kind, and every thing that creepeth upon the earth after his kind: and God saw that it was good." -- Genesis 1:25 (King James Bible)

"God made all sorts of wild animals, livestock, and small animals, each able to produce offspring of the same kind. And God saw that it was good." -- Genesis 1:25 (New Living Translation)

OOP is a programming paradigm that lets us organize/create data according its kind.

Lets us think about programming in a very natural and intuitive way.

Useful reading

For example, all lists can have objects appended to them.

In [32]:
mylist = [1]
[1, 2]

But we can't append to an object of type integer.

That would be behaviour that is not according to its kind. God says that is not good.

In [33]:
x = 2
AttributeError                            Traceback (most recent call last)
<ipython-input-33-7a1f00d403a2> in <module>()
      1 x = 2
----> 2 x.append(3)

AttributeError: 'int' object has no attribute 'append'

Different object types have different attributes, according to their kind.

We've gone pretty far in the course using types that Python has made for us.

But the point of today is to learn how we can define our own classes.

In Python, type $\iff$ class.

Defining a Class

The statement class is used to create a class.

Like a function definition, anything tabbed in belongs to the class definition.

Here is the most basic class definition possible:

In [72]:
class Foo:

We defined a class, or a type, that we called Foo and pretty much left the definition empty otherwise.

Now we can create objects of type Foo.

Once you defined a class, you can create an object of that type using the class name as a function.

Doing this creates an instance of the class Foo.

For example, each person is an instance of the imaginary class "Human".

In [98]:
#create an instance of the type Foo
myfoo = Foo()
#create another instance of type Foo
myotherfoo = Foo()
In [101]:
print(f"My object is of type: {type(myfoo)}")
print(f"The id of my object is: {id(myfoo)}")
My object is of type: <class '__main__.Foo'>
The id of my object is: 4578050176
In [102]:
print(f"The other instance's id is: {id(myotherfoo)}")
print(f"myfoo and myotherfoo are the same type: {type(myfoo) == type(myotherfoo)}")
The other instance's id is: 4578050344
myfoo and myotherfoo are the same type: True

Like an object's id, the type of an object can never change

Classes are also objects but let's not go down that rabbit hole. If you're interested, look up "Metaprogramming".

In [73]:

So we were able to create an object but we didn't actually code anything...

Clearly there is some magic going on here.

Class Hierarchies

We can often understand the world as a hierarchy of types.

More specifically, derived classes inherit the properties of base classes.

For example: dog types have all the properties of the animal class which has all the properties of the 'living-thing' class, etc.

Types in Python work the same way.

In Python, the class hierarchy looks more like this:

Whenever we create a new class, it inherits the attributes of the class object (the most generic class).

This makes object the base class of Foo.

The object class contains some methods that let us create new objects.

Creating a new object

The object class has a method called __new__() which creates new objects of a given type and returns it.

This is what is used to create new objects.

In [109]:
myfoo = object.__new__(Foo)

Is (partially) equivalent to ..

In [111]:
myfoo = Foo()

Initializing an object

Once an object is created, it is like a blank slate.

In order for it to be useful we have to give it some attributes.

This is done by the __init__ method. Which is short for initialization.

__init__(self, *args, **kwargs) takes as input an instance of the class, given the name self here and returns nothing. It just sets attributes.

e.g. It lets us do something like this:

>>> x = 5
#numerator is an attribute of integer objects
>>> s.numerator
>>> s.denominator

So what's really happening when we do myfoo = Foo() is the base class __new__ method is called to create a new object.

Then the __init__() method is called to set its attributes.

In [131]:
myfoo = object.__new__(Foo)
#now the object is fully created


Since Foo is actually using the __init__ method from the object class we get a very boring looking object that is the most generic type of data we can create.

If we want to customize our object, we have to re-define or override the __init__ method inside our class definition.

Python will always use the definition of a method of the derived class before the base class if it is defined.

So let's redefine our class.

In [126]:
class Foo:
    def __init__(self, n): = n

Now when I create an object, I can pass an argument and the __init__ method I defined will be used instead of the generic object method.

We have overridden the base method definition for our own.

In [127]:
myfoo = Foo("Carlos")
In [128]:

Instance and Class Attributes

What we've done is define something known as an instance attribute.

All instances of the class Foo will have a name attribute (or instance variable) but their values are independent of each other.

In [132]:
a = Foo("Plato")
b = Foo("Socrates")


a and b are both instances of the class Foo so they both have a name but their name attributes are different.

We can also define data that is common to all instances of a class. These are known as class attributes.

Class attributes are defined outside any function definitions of a class.

In [136]:
class Family():
    address = "1234 Elm St."
    family_members = 0
    def __init__(self, n): = n
        Family.family_members += 1
In [139]:
alice = Family("Alice")
bob = Family("Bob")
In [140]:
1234 Elm St.

You are not limited to having attributes defined in __init__(self).

At any point later in your code you can create and set new attributes.

The purpose of __init__(self) is to ensure that all instances of a class have some common data.

In [141]: = "F"
In [142]:
In [143]:
AttributeError                            Traceback (most recent call last)
<ipython-input-143-c81fa51fa28a> in <module>()
----> 1 print(

AttributeError: 'Family' object has no attribute 'sex'

As you know, object attributes can also be executable i.e. functions.

In [144]:
class DNA:
    def __init__(self, seq):
        self.sequence = seq
    def compute_GC(self):
        return len([n for n in self.sequence if n in {"G", "C"}]) / len(self.sequence)

The function dir(object) tells us the accessible attributes of an object.

In [146]:
In [147]:
In [148]:

There are many other functions that are defined for us that we can override to do cool things.

In [149]:
class WeirdNumber:
    def __init__(self, n):
        self.num = n
    def __add__(self, other):
        return self.num / other.num
In [150]:
w = WeirdNumber(3)
x = WeirdNumber(2)

w + x

Now we know that the + operator calls the __add__ instance method of the two objects being added.

Instance methods always take as input an instance of the class. Hence, the mandatory self positional argument.

self is implicitly passed through the . operator.

In [151]:
class Foo:
    def print_self(self):
In [154]:
f = Foo()
<__main__.Foo object at 0x110ddc898>


Classes define types of objects that share some common attributes.

A class can produce instance of itself using the ClassName(*args, **args) syntax.

When a class instance is created, the __new__() and __init__ methods are used to create the object, and set its attributes initially.

If the class definition overrides the base class's methods then the derived class definition will be used.

In [155]:
import datetime as dt

class Book:
    def __init__(self, author, title, loaned): = author
        self.title = title
        self.loaned = loaned
    def borrow(self):
        if not self.loaned:
            self.loaned =
    def book_return(self, borrow_days=10, late_fee=0.25):
        date_loaned = self.loaned()
        self.loaned = False
        if self.loaned > + 10:
            return ( - self.loaned) * late_fee
            return None
class Library:
    def __init__(self):
        self.books = {}
    def add_book(self, title, author):
        b = Book(author, title, False)
        if title not in self.books:
            self.books[title] = b
In [156]:
mylib = Library()
mylib.add_book("How to Travel with a Salmon", "Umberto Eco")
In [157]:
{'How to Travel with a Salmon': <__main__.Book object at 0x110dcaac8>}
In [159]:
salmon = mylib.books["How to Travel with a Salmon"]
In [160]:
In [161]:
Out[161]:, 11, 1)