Classes in Python

Learning Objectives
Students Will Be Able To: |
---|
Describe the difference between classes and objects |
Define a class in Python |
Instantiate a class to create an object |
Explain the special __init__ method |
Understand class vs. instance attributes |
Road Map
- Setup
- Review of OOP
- Writing a basic Python
class
- Creating Objects by Instantiating a Class
- Overriding Methods
- Class vs. Instance Members
- Inheritance
- Essential Questions
- Optional Practice Exercise
- Further Study
1. Setup
We'll be writing code in repl.it:
- Create a Python-based repl
- Name it something like "Python Classes"
2. Review of OOP
Python is an object-oriented programming (OOP) language.
Object-oriented programming is characterized by programming with objects that represent the real-world objects of the application.
❓ What are classes used for in OOP?
Classes are like "blueprints" and are used to create objects.
A key principle of OOP we discussed during the JS Classes lesson is encapsulation...
❓ What's encapsulation?
The bundling of related properties (attributes) and methods (behavior) together in an object.
Objects in Python
As you've already heard, everything in Python is an object.
This means is that every Python variable or piece of data has attributes and/or methods encapsulated within the object.
Python provides a dir()
function that can be used to list an object's members (attributes and methods):
# create a list
nums = [1, 2, 3]
# print the attributes & methods nums has
print( dir(nums) )
Some of the members like append
& pop
might look familiar.
The other methods that start and end with double-underscores, are called magic (or dunder) methods. They are internal methods most commonly used to overload operators. If you would like to learn more about them, be sure to check out the link in the Further Study section.
We'll be using the __init__
dunder method shortly.
When we start working with Django, we'll be defining quite a few classes, so let's see how to we do it...
3. Writing a basic Python class
Like many of you, I like dogs - let's define a Dog
class to create doggies from!
I'll explain the code as we type it:
class Dog():
def __init__(self, name, age=0):
self.name = name
self.age = age
def bark(self):
print(f'{self.name} says woof!')
The naming convention for Python classes is UpperCamelCasing - same as in JavaScript.
Python automatically calls the __init__
magic method when a new dog is created.
__init__
is short for "initialize" because the method is used to initialize the properties of the new object.
❓ What is the name of the method in JS classes responsible for initializing properties?
constructor()
The age=0
in __init__
's parameter list is called a default parameter and will be assigned the the result of the expression to the right of the =
if the function is called without an argument for that positional parameter.
The attributes for a dog instance are name
and age
.
bark
is an instance method in this Dog class.
❓ What's an instance method?
A method that is callable on an instance of a class, i.e., object
What's this self
business?
In the JS lesson about the this
keyword, it was mentioned that every OOP language must have the same or similar mechanism as this
to be able to:
- Enable a method to access the other properties/methods in an object, and
- Enable a single-copy of a method in memory to serve any number of instances.
JavaScript, Java, C++, C#, and others call it this
.
Ruby, Swift and others call it self
.
However, in Python, only by convention is it called self
because it's just a parameter name...
Take a look at the __init__
and bark
method definitions, notice how the first parameter is named self
.
When we write code like spot.bark()
, the object to the left of the dot is automatically passed by Python as the first argument.
This is how Python provides a method's "context" in both instance and class methods!
4. Creating Objects by Instantiating a Class
By defining the Dog
class, we now know the structure that each of the pooches will have!
Let's make a dog:
spot = Dog('Spot', 8)
print(spot) # -> similar to <__main__.Dog object at 0x7f27bad2c208>
# print the name and age attributes of the spot object
print(spot.name, spot.age) # -> Spot 8
# invoke the spot object's bark instance method
spot.bark() # -> Spot says woof!
👀 KEY POINT: Unlike Python dictionaries that use square bracket notation to access/set its items' values, objects instantiated by our own Python classes are more like JS objects in that they use dot notation instead.
Let's try out the default parameter for a new dog's age
:
dog = Dog('Lassie')
print(dog.name, dog.age) # -> Lassie 0
5. Overriding Methods
As we just saw above, when we used print(spot)
to print the spot
object, a not so helpful string was outputted.
We can change this behavior by overriding the __str__
method that the print
function calls automatically to obtain the string to print out.
Let's modify the Dog
class to override the __str__
method:
class Dog():
def __init__(self, name, age=0):
self.name = name
self.age = age
def bark(self):
print(f'{self.name} says woof!')
def __str__(self):
return f'Dog named {self.name} is {self.age} years old'
Let's try it out:
spot = Dog('Spot', 8)
print(spot) # -> Dog named Spot is 8 years old
Let's practice creating another class!
💪 Practice Exercise - Create a Class (15 min)
At the top of the repl, define a class named Vehicle
with the following members:
make
: Attribute for the vehicle's makemodel
: Attribute for the vehicle's modelrunning
: Attribute for maintaining whether or not the vehicle is "running". Initializeself.running
to a default ofFalse
within the__init__
method instead of relying on a value being passed in as an argument.start()
: Method that sets therunning
attribute toTrue
and prints "running...".stop()
: Method that sets therunning
attribute toFalse
and prints "stopped...".
Override the __str__
method so that it returns a string formatted as:
Vehicle is a <make> model <model>
After defining the Vehicle
class, instantiate the class and assign the returned object to a variable named car
. For example:
car = Vehicle('Tesla', 'Model S')
Test out the class in the console:
- Run the Repl
- Take
car
for a test drive (see below)
Example test drive:
> print(car.make, car.model)
Tesla Models S
> print(car)
Vehicle is a Tesla model Model S
> print(car.running)
False
> car.start()
Running...
> car.running
True
>car.stop()
Stopped...
👉 Please submit the link to your Repl in the provided input.
Solution (don't peek)
class Vehicle():
def __init__(self, make, model):
self.make = make
self.model = model
self.running = False
def start(self):
self.running = True
print('Running...')
def stop(self):
self.running = False
print('Stopped...')
def __str__(self):
return f'Vehicle is a {self.make} model {self.model}'
car = Vehicle('Tesla', 'Model S')
6. Class vs. Instance Members
In Python, instance attributes & methods (members) are intended to be accessed/invoked by instances of the class, whereas, class members are intended to be accessible on the class only, not an instance.
This means the class attributes are shared with all instantiations of the Dog class, while instance variables are only usable within the instance, or current instantiation, of a given Dog class.
Each object has its own copy of its instance attributes, e.g., name
. However, class attributes/methods are shared by all instances of that class.
To demonstrate class attributes, let's add a next_id
class attribute to the Dog
class that can be used to assign an id
to each dog object:
class Dog():
# class attribute
next_id = 1
# updated __init__
def __init__(self, name, age=0):
self.name = name
self.age = age
self.id = Dog.next_id
Dog.next_id += 1
def bark(self):
print(f'{self.name} says woof!')
# updated __str__
def __str__(self):
return f'Dog ({self.id}) named {self.name} is {self.age} years old'
Note how the Dog.next_id
class attribute is being accessed within the __init__
method.
👀 Technically, instances can also access class members via
self
due to the fact that Python will look for a member on the class if it first doesn't find it on the instance.
Now let's make sure it worked :)
spot = Dog('Spot', 8)
print(spot)
pup = Dog('Lassie')
print(pup)
Cool, now let's see how class methods are created by adding a get_total_dogs
method.
Add this to the bottom of the Dog
class:
def __str__(self):
return f'Dog ({self.id}) named {self.name} is {self.age} years old'
# new code below
@classmethod
def get_total_dogs(cls):
# cls represents the Dog class
return cls.next_id - 1
There's only two differences when defining a class method:
- The
@classmethod
decorator - The naming convention of the first parameter is to use
cls
instead ofself
👀 Decorators in programming are a form of metaprogramming (when a program has knowledge of, or manipulates itself). In Python, decorators are used to modify the behavior of a function or class. They are not very common, but there's a link in the Further Study section if you'd like to learn more about decorators in Python.
Let's test out the new class method:
spot = Dog('Spot', 8)
pup = Dog('Lassie')
# class methods are called on the class, not an instance
print(Dog.get_total_dogs()) # -> 2
7. Inheritance
Maybe the following graphic will jog your memory in regards to what inheritance is:

Using inheritance, a subclass automatically inherits all of the attributes and methods of its superclass.
The subclass can then define additional attributes and/or methods to make a more specialized class than the superclass.
For example, in the JS Classes lesson, we specialized the Square
class by extending it to create an ImageSquare
subclass.
Let's see how inheritance is implemented in Python by creating a ShowDog
class that specializes the Dog
class:
# Pass in superclass as argument
class ShowDog(Dog):
# Add additional parameters AFTER those in the superclass
def __init__(self, name, age=0, total_earnings=0):
# Always call the superclass's __init__ first
Dog.__init__(self, name, age)
# Now add any new attributes
self.total_earnings = total_earnings
# Add additional methods
def add_prize_money(self, amount):
self.total_earnings += amount
print(f'{self.name}\'s new total earnings are ${self.total_earnings}')
👀 If not specified, the default superclass is Python's
object
class. In JS, it was theObject
class.
It's show time!
winky = ShowDog('Winky', 3, 1000)
print(winky) # Yay, inherited the overriden __str__
winky.bark() # Yay, inherited the bark method
print(winky.total_earnings) # -> 1000
winky.add_prize_money(500) # New method that 'Dogs' don't have
print(winky.total_earnings) # -> 1500
Inheritance is critical to OOP languages. In fact, they even have their own object hierarchies. Check this out:

Frameworks like Django and Rails have elaborate object hierarchies of their own. For example, when we move on to Django, we'll be defining Models by inheriting from a Django class like this:
class Person(models.Model):
That was fun!
FYI, here's the link to my Repl for this lesson.
8. Essential Questions (1 minute)
(1) How do we create objects using a class?
By invoking/calling the class
(2) True or False: Class attributes are shared by all instances of that class.
True
(3) What OOP principle refers to subclasses specializing superclasses?
Inheritance
9. Optional Practice Exercise
Looking for some practice building an object hierarchy in Python? Good!
In a separate Python repl...
Create a BankAccount
class with the following members:
owner
: (attribute) The owner's name as a stringbalance
: (attribute) The amount of money in the accountaccount_no
: (attribute) A number to be randomly generated and assigned within__init__
- not passed in at time of instantiationdeposit(amount)
: (method) When called on an instance, increases thebalance
by theamount
argument and returns the new balancewithdraw(amount)
: (method) When called on an instance, decreases thebalance
by theamount
argument and returns the new balance
Here's how to generate a random integer for the in Python:
# Put this line at the top of the repl
import random
# Use this inside of BankAccount's __init__ to generate
# a random account number from 111111111 to 999999999
self.account_no = random.randint(111111111, 999999999)
Create two instances, make both deposits and withdrawals, and print the attributes to test them out.
Bonus 1
Override the __str__
method to return the following formatted string:
Account <account_no> / Balance: xxxxx.xx
Bonus 2
Create a SavingsAccount
class that subclasses BankAccount
and specializes it so that the withdraw
method no longer accepts any argument, does not change the balance, and returns a string of No withdrawals permitted
.
Bonus 3
Add an additional has_overdraft
attribute to the BankAccount
class that accepts True
or False
at the time of instantiation, but defaults to False
if not passed in (hint: review default parameters discussed above).
When the withdraw
method is called, do not allow the withdraw if the amount being withdrawn is greater than balance
, unless has_overdraft
is True
. withdraw
should continue to return the balance
.
10. Further Study
The "Official" Classes Tutorial
Learn more about magic methods here
Learn more about Python's self
here
Learn more about metaprogramming here
Decorators in Python