Since this is such a long and complicated module I have included a glossary at the bottom to help sort things out. I would also recommend quickly skipping to the section about f-strings and dates in python (found in the extras module) as it will help the code make more sense.


Classes are a way of bundling data (attributes) and functions (sometimes called methods) into abstractions in python.

For example, let's say you are writing an app to store a bunch of data about animals. For this you can set up a class that has the basic attributes of all animals like species name, endemic regions (where it lives), common name (what people know it as) etc.

The python code for this would look like

class Animal:
  def __init__(self, species_name, regions, common_name):
    """A class to represent a generic animal

    Attributes
    ----------
    species_name : (str) 
        The technical species name of the animal
    regions : (list[str]) 
        A list of regions the animal is endemic to
    common_name : (str) 
        The colloquial name of the animal
    """
    self.species_name = species_name
    self.regions = regions
    self.common_name = common_name

So we can break down the example in a moment, first let's start with what you can now do with the above class. Once you have a class, it can act as a template to create instances. You can think of this with the analogy that a class is a cookie cutter, and an instance is a cookie that has been cut from the cutter.

Let's use our Animal class (cookie cutter) to create an instance of a Common Leopard Gecko(cookie):

leopard_gecko = Animal("Eublepharis macularius",
    ["Afghanistan","Pakistan","India", "Iran"],
    "Common Leopard Gecko")

Now that we have have instantiated the leopard gecko instance, we can use a simple syntax (instance.variable_name) to access the variables. For example if we wanted to know the common name of a Common Leopard Gecko we could use:

leopard_gecko.common_name # Returns 'Common Leopard Gecko'

We can also add methods (functions) specific to the class, for example let's write a method that prints info about the animal:

class Animal:
  def __init__(self, species_name, regions, common_name):
    """A class to represent a generic animal

    Attributes
    ----------
    species_name : (str) 
        The technical species name of the animal
    regions : (list[str]) 
        A list of regions the animal is endemic to
    common_name : (str) 
        The colloquial name of the animal
    """
    self.species_name = species_name
    self.regions = regions
    self.common_name = common_name
  def print_info(self):
    """Prints information about animal instance"""
    print(f"\nCommon Name: {self.common_name}\nSpecies: {self.species_name}\nRegions: {self.regions}")

Now we can take the leopard gecko example from earlier and use our method using the syntax (instance.method()):

leopard_gecko = Animal("Eublepharis macularius", ["Afghanistan","Pakistan","India", "Iran"],"Common Leopard Gecko")

"""Prints (not returns)
Common Name: Common Leopard Gecko
Species: Eublepharis macularius
Regions: ['Afghanistan','Pakistan','India', 'Iran']
"""
leopard_gecko.print_info()

There was a lot going on in the last example so lets break down what happened. First we start by defining our class (usually called the class definition) with:

class Classname: # Notice for classes the convention is to start them with a capital
  def __init__(self): # This method will be explained later on
    pass

Following our class definition right away we define a __init__ method. The __init__ method is explained in further detail below. But first let's take a look at that funny self that we've been putting in front of our variables.

self; Instance vs Class attributes

As you can see, for all of the instance attributes(variables specific to each instance and not the overall class) we are adding a self in front with a dot. This is because when we create an instance, all of the variables are localized to that instance.

So for example if we use the animal class from earlier to create two different animal instances, the variables don't overlap.

leopard_gecko = Animal("Eublepharis macularius",
    ["Afghanistan","Pakistan","India", "Iran"],
    "Common Leopard Gecko")

arctic_fox = Animal("Vulpes lagopus",
    ["The Arctic"],
    "Arctic fox")


leopard_gecko.common_name # Returns 'Common Leopard Gecko'

arctic_fox.common_name # Returns 'Arctic fox'

This is the same with instance methods; The variables it pulls are specific to the instance and not the class. For example:

leopard_gecko = Animal("Eublepharis macularius",
    ["Afghanistan","Pakistan","India", "Iran"],
    "Common Leopard Gecko")

arctic_fox = Animal("Vulpes lagopus",
    ["The Arctic"],
    "Arctic fox")

leopard_gecko.print_info()
"""Prints (not returns)
'Common Name: Common Leopard Gecko
Species: Eublepharis macularius
Regions: ["Afghanistan","Pakistan","India", "Iran"]'
"""

arctic_fox.print_info()
"""Prints (not returns)
'Common Name: Arctic fox
Species: Vulpes lagopus
Regions: ["The Arctic"]'
"""

Class attributes on the other hand are attributes that are common among all instances of a class.

For example, let's say you wanted to keep a counter that goes up by 1 for every time a new animal is added. Since this information, is not specific to an instance, but rather to every instance of a given class you would want it to be accessible to every instance. The code to do something like this would be:

class Animal:
  counter = 0 # Initialize counter to 0
  # This ^^ is a class variable since it is outside of __init__ and has no self. in front
  def __init__(self, species_name, regions, common_name):
    """A class to represent a generic animal

   Attributes
    ----------
    species_name : (str) 
        The technical species name of the animal
    regions : (list[str]) 
        A list of regions the animal is endemic to
    common_name : (str) 
        The colloquial name of the animal
    """
    self.species_name = species_name
    self.regions = regions
    self.common_name = common_name
    Animal.counter += 1 # Accessing and incrementing the class counter by 1 on each instantiation of an Animal

  def print_info(self):
    """Prints information about animal instance"""
    print(f"\nCommon Name: {self.common_name}\nSpecies: {self.species_name}\nRegions: {self.regions}")

Now there is a counter variable that can be used to find out how many animals have been instantiated

print(Animal.counter) # Prints 0; since no Animal's have been instantiated
leopard_gecko = Animal("Eublepharis macularius",
    ["Afghanistan","Pakistan","India", "Iran"],
    "Common Leopard Gecko")

print(Animal.counter) # Prints 1; since the Leopard Gecko has been instantiated

arctic_fox = Animal("Vulpes lagopus",
    ["The Arctic"],
    "Arctic fox")

print(Animal.counter) # Prints 2; since the Leopard Gecko, and Arctic fox have been instantiated

# Both below calls print 2; Class variables are also accessible in any instance
print(arctic_fox.counter)
print(leopard_gecko.counter)

As you can see because the variable belongs to the class and not the instance, it is available to both the class as a variable, or any instances of the Animal class.

Class methods

Methods are functions that are accessible through class instances, for example let's say you want to create a function to print all of the attributes of a class you could define the function in the class and then use the self operator to print the information. We have already seen this in fact in the above examples with our Animal class, the print_info() method used earlier is a class method.

Basic Syntax

Setting up class methods is the same as setting up a regular function, you just need to indent it to the same line as the class and always pass self as an argument. For example:

class Animal:
  def __init__(self, species_name, regions, common_name):
    """A class to represent a generic animal

    Attributes
    ----------
    species_name : (str) 
        The technical species name of the animal
    regions : (list[str]) 
        A list of regions the animal is endemic to
    common_name : (str) 
        The colloquial name of the animal
    """
    self.species_name = species_name
    self.regions = regions
    self.common_name = common_name
  def print_info(self):
    """Prints information about animal instance"""
    print(f"\nCommon Name: {self.common_name}\nSpecies: {self.species_name}\nRegions: {self.regions}")

__init__ method

The __init__ method in python acts as a constructor (sort of) in python. This means that it 'constructs' the instance.

In our analogy of a cookie cutter from earlier, the __init__ method would be the actual cutting of the cookie. The method is run every time you instantiate an instance. For example when you run the leopard_gecko example from before:

leopard_gecko = Animal("Eublepharis macularius",
    ["Afghanistan","Pakistan","India", "Iran"],
    "Common Leopard Gecko")

The variable is making an implicit call to __init__, this would roughly be equivalent to:

leopard_gecko = Animal.__init__("Eublepharis macularius",
    ["Afghanistan","Pakistan","India", "Iran"],
    "Common Leopard Gecko")

We can see this more clearly by switching examples a bit. Let's take the following class:

class CookieBaker:
  def __init__(self, number_of_cookies):
    """ Example class that is used to show off the __init__ method.
    The __init__ method calls prints 'Cookie Baked' as many times as there are number_of_cookies.

    Attributes
    ----------
    number_of_cookies(int): How many cookies to bake
    """
    print(f"__init__ method called, creating {number_of_cookies} cookie(s):")
    self.number_of_cookies = number_of_cookies
    for cookie in range(number_of_cookies):
      print("Cookie Baked!")

As you can see, you can do some basic logic in the __init__ method, such as for loops. You can also call methods, just make sure to include self. when calling them since you are inside the instance.

For example:

class CookieBaker:
  def __init__(self, number_of_cookies):
    """ Example class that is used to show off the __init__ method.
    The __init__ method calls the bake_cookie() method as many times as there are number_of_cookies.

    Attributes
    ----------
    number_of_cookies(int): How many cookies to bake
    """
    print(f"__init__ method called, creating {number_of_cookies} cookie(s):")
    self.number_of_cookies = number_of_cookies
    for cookie in range(number_of_cookies):
      self.bake_cookie()

  def bake_cookie(self):
    """Print's 'Cookie Baked!'."""
    print("Cookie Baked!")

Additional info (optional)

Class attributes: Access

Keep in mind that like other variables python will let you override class variables without question. So they can be modified from the class at will. For example:

print(Animal.counter) # Prints 0; since no Animal's have been instantiated
leopard_gecko = Animal("Eublepharis macularius",
    ["Afghanistan","Pakistan","India", "Iran"],
    "Common Leopard Gecko")

print(Animal.counter) # Prints 1; since the Leopard Gecko has been instantiated

Animal.counter = 35 # Overriding the variable from the class

print(Animal.counter) # Prints 35; since the attribute has been overridden
print(leopard_gecko.counter) # Prints 35; since the attribute has been overridden

But if you try to modify a class variable from an instance, then it will create an instance variable that is now local to the instance while leaving the class variable in tact:

print(Animal.counter) # Prints 0; since no Animal's have been instantiated
leopard_gecko = Animal("Eublepharis macularius",
    ["Afghanistan","Pakistan","India", "Iran"],
    "Common Leopard Gecko")

print(Animal.counter) # Prints 1; since the Leopard Gecko has been instantiated

Animal.counter = 35 # Overriding the variable from the class

print(Animal.counter) # Prints 35; since the attribute has been overridden

leopard_gecko.counter = 26 # creating an instance variable from the class attribute

print(Animal.counter) # Prints 35; since the class attribute WONT be modified by changing the gecko instance counter
print(leopard_gecko.counter) # Prints 26; since the instance attribute has been created

Dataclasses in python

If you are just storing variables there is also a useful module inside the python standard library called dataclasses this library makes creating useful classes that just store data much faster.

Let's take a look at this example:

import datetime
class user:
  def __init__(self, name, age, sign_up_date, birthday, premium_member):
    """A class to represent a generic animal

    Attributes
    ----------
    name(str): The technical species name of the animal
    age(str): A list of regions the animal is endemic to
    sign_up_date(datetime.datetime): A datetime object of the day the user signed up
    birthday(datetime.datetime): A datetime object of the users birthday
    premium_member(bool): Whether the user is on premium or free subscription
    """
    self.name = name
    self.age = age
    self.sign_up_date = sign_up_date
    self.birthday = birthday
    self.premium_member = premium_member

Now thats a lot of self.attribute_name's, dataclasses will automate the __init__ method (and __repr__ method).

The same class can be written like this:

import datetime
from dataclasses import dataclass

@dataclass
class user:
    """A class to represent a generic animal

    Attributes
    ----------
    name(str): The technical species name of the animal
    age(str): A list of regions the animal is endemic to
    sign_up_date(datetime.datetime): A datetime object of the day the user signed up
    birthday(datetime.datetime): A datetime object of the users birthday
    premium_member(bool): Whether the user is on premium or free subscription
    """
    name:str
    age:str
    sign_up_date:datetime.datetime
    birthday:datetime.datetime
    premium_member:bool

For more details check out: https://github.com/canadian-coding/posts/tree/master/2019/August/19th%20-%20Dataclasses%20in%20Python

Glossary

  • Class: A template to create Instance(s)/Object(s) from. Classes exist to bundle data (attributes) and functions (methods) into abstractions that are meaningful. A good analogy is to think of a cookie cutter as a class, that is a template used to cut (instantiate) cookies (Instance(s)/Object(s))

For example you could have an Animal class that can be used to create Instance(s)/Object(s) to bundle data and functions about animals, or a user class that can be used to create Instance(s)/Object(s) to bundle data and functions about each user of an app.

  • Instance/Object: An object representing something, created from a class (used as a template). A good analogy is to think of a cookie cutter as a class, that is a template used to cut (instantiate) cookies (Instance(s)/Object(s)).

For example if you had an Animal class, you could use it to instantiate a leopard gecko instance that has all the data (attributes) and functions (methods) necessary to represent a leopard gecko.

In python people use Instance and Object interchangeably, and the same is true for many other object-oriented languages.

  • Instantiate: The act of creating (initializing) an Instance/Object from a class. A good analogy is to think of a cookie cutter as a class, that is a template used to cut (instantiate) cookies (Instance(s)/Object(s))

  • Attribute: A variable that is specific to a class or Instance/Object.

  • Method: A function that is specific to a class or Instance/Object.

  • Constructor: What typically gets called on instantiation of an Instance/Object. This is a concept used broadly in object-oriented languages, but in python this roughly corresponds to the __init__ method.

Exercises

import datetime # SEE: https://docs.python.org/3/library/datetime.html and the extras module for details """ =========== Exercise 1 ============= Write the estimate_planks function so that it returns the correct number of planks (use integer division to avoid complication). Example: """ class Contractor: def __init__(self, plank_length = 1): """A Demo class representing a construction Contractor. Attributes ---------- plank_length(int): The length of wooden planks in feet; Default: 1 Methods ------- estimate_planks: Estimates the amount of planks needed to build a structure based on provided length. """ self.plank_length = plank_length def estimate_planks(self, length): """Estimates the amount of planks needed to build the structure, based on the length Parameters ---------- length(int): How long the structure that needs to be built is. """ number_of_planks = None # Just a dummy value, replace with the real calculation return f"You need {number_of_planks} plank(s) of wood to build this wall" dave = Contractor(2) print(dave.estimate_planks(10)) # Should be 5 planks billy = Contractor(5) print(billy.estimate_planks(225)) # Should be 45 planks """ =========== Exercise 2 ============= Write a user class that has 3 attributes and 1 method: Attributes: 1. name(str): users full name 2. birthday(datetime.date): Users birthday as a datetime object 3. premium (boolean): True or false based on if user is a premium user or not Method: 1. next_birthday: A method that returns a string of the users birthday this year, if it is today then return 'Happy Birthday!' instead. """ class User: def __init__(self): pass def next_birthday(self): pass john = User("John Doe", datetime.date(1995,10,25), True) print(john.next_birthday()) # You will have to validate this yourself print("John Doe" in john.name) # Should print True print(john.premium) # Should Print True """ =========== Exercise 3 ============= Take the previous user class from before, and add a class docstring to it. For more details on docstrings and their formatting check out: https://github.com/canadian-coding/posts/tree/master/2019/July/25th%20-%20Docstrings%20in%20python """

Challenges

""" ============= Challenge 3 ============== This challenge is very VERY hard if you have never worked with classes. Give it a shot but don't worry if you can't do it. Read into inheritance in python: https://www.w3schools.com/python/python_inheritance.asp Now write a Product class that extends Item, that includes: 1. nutritional information (A dictionary with values from here: https://schema.org/NutritionInformation such as calories, carbohydrateContent etc.). 2. A method that prints the nutrition info of a Product into a easily readable format 3. An expiry attribute that is a datetime.date object: https://docs.python.org/3/library/datetime.html 4. A method that checks the current date, and returns True if the product has expired, and False if it has not yet expired """ class Item: def __init__(self, category): """ A class representing an inventory item for a company. Attributes ---------- category(str): A representation of the general category of an item id(str): A unique ID for the item (note this is generated in __init__ and not a parameter passed on instantiation) """ import uuid # Generates a unique ID self.id = uuid.uuid4() # Assigns a unique ID to an object self.category = category

Solutions




import datetime # SEE: https://docs.python.org/3/library/datetime.html and the extras module for details """ =========== Exercise 1 ============= Write the estimate_planks function so that it returns the correct number of planks (use integer division to avoid complication). Example: """ class Contractor: def __init__(self, plank_length = 1): """A Demo class representing a construction Contractor. Attributes ---------- plank_length(int): The length of wooden planks in feet; Default: 1 Methods ------- estimate_planks: Estimates the amount of planks needed to build a structure based on provided length. """ self.plank_length = plank_length def estimate_planks(self, length): """Estimates the amount of planks needed to build the structure, based on the length Parameters ---------- length(int): How long the structure that needs to be built is. """ number_of_planks = length // self.plank_length return f"You need {number_of_planks} plank(s) of wood to build this wall" dave = Contractor(2) print(dave.estimate_planks(10)) # Should be 5 planks billy = Contractor(5) print(billy.estimate_planks(225)) # Should be 45 planks """ =========== Exercise 2 ============= Write a user class that has 3 attributes and 1 method: Attributes: 1. name(str): users full name 2. birthday(datetime.date): Users birthday as a datetime object 3. premium (boolean): True or false based on if user is a premium user or not Method: 1. next_birthday: A method that returns a string of the users birthday this year, if it is today then return 'Happy Birthday!' instead. """ class User: def __init__(self, name, birthday, premium): """A Demo class representing a user for an application. Attributes ---------- name: (str) The users' name. birthday: (datetime.date) The date of birth for the user. premium: (bool) Represents whether a user is a premium user or not. Methods ------- next_birthday: Determines the users' birthday in the current year and prints it. """ self.name = name self.birthday = birthday self.premium = premium def next_birthday(self): """Determines the users' birthday in the current year and prints it""" if self.birthday == datetime.date.today(): print("Happy Birthday!") else: current_year = datetime.date.today().year birth_month = self.birthday.month birth_day = self.birthday.day print(datetime.date(current_year, birth_month, birth_day)) john = User("John Doe", datetime.date(1995,10,25), True) print(john.next_birthday()) # You will have to validate this yourself print("John Doe" in john.name) # Should print True print(john.premium) # Should Print True """ =========== Exercise 3 ============= Take the previous user class from before, and add a class docstring to it. """ # I already did the above exercise, take a look at exercise 2 for answer