Practice

classes and self

Let’s practice making a simple class. Open a new file and save it as class_example.py, we’ll be running this from the command line rather than the REPL. Pass in several variables and save them to the instance by using self:

# class_example.py

class Vehicle:

    def __init__(self, make, model, fuel="gas"):
        self.make = make
        self.model = model
        self.fuel = fuel

daily_driver = Vehicle("Subaru", "Crosstrek")
# By default, this is how python represents our object:
print(daily_driver)

# The variables we saved to the instance are available like this:
print(f"I drive a {daily_driver.make} {daily_driver.model}. It runs on {daily_driver.fuel}.")

class Variables

We can also add class variables - variables that exist for all instances of a class. Let’s add a variable called number_of_wheels to the class scope:

# class_example.py

class Vehicle:

    number_of_wheels = 4

    def __init__(self, make, model, fuel="gas"):
        self.make = make
        self.model = model
        self.fuel = fuel

Let’s query both the instance and class variables. Note that we set the instance variable to 3, but the higher-level class variable is still set to 4.

# class_example.py

daily_driver = Vehicle("Subaru", "Crosstrek")

daily_driver.number_of_wheels = 3

# Instance variables
print(f"I drive a {daily_driver.make} {daily_driver.model}. It runs on {daily_driver.fuel}.")
print(f"My {daily_driver.model} has {daily_driver.number_of_wheels} wheels.")

# Class variable
print(f"Most vehicles have {Vehicle.number_of_wheels} wheels.")

Inheritance

Class inheritance in Python is super useful - you can easily create a hierarchy of classes to make your life easier and maximize code reuse. Let’s subclass our Vehicle class and extend it by breaking out Cars and Trucks.

# class_example.py

class Vehicle:

    def __init__(self, make, model, fuel="gas"):
        self.make = make
        self.model = model
        self.fuel = fuel


class Car(Vehicle):

    number_of_wheels = 4


class Truck(Vehicle):

    number_of_wheels = 6

    def __init__(self, make, model, fuel="diesel"):
        super().__init__(make, model, fuel)

Note: we’ve moved the number_of_wheels variable to the subclasses. Our Car subclass sets this variable but instantiating a Car just passes through to Vehicle.__init__(). We do, however, provide a __init__() for Truck, which changes the default fuel to diesel and then calls super().__init__() which redirects to Vehicle.__init__(). This lets us make changes that are specific to Truck instances (but we can still call them Vehicles). Let’s instantiate our subclasses:

# class_example.py

daily_driver = Car("Subaru", "Crosstrek")
print(f"I drive a {daily_driver.make} {daily_driver.model}. "
      f"It uses {daily_driver.fuel} and has {daily_driver.number_of_wheels} wheels.")

truck = Truck("Ford", "F350")
print(f"I also have a {truck.make} {truck.model}. "
      f"It uses {truck.fuel} and has {truck.number_of_wheels} wheels.")

type, isinstance, and issubclass

The type() command tells us the type of an object - for example, a Truck or a Car. Note that it doesn’t know anything about inheritance, so you can’t use type() to check if a Car is a Vehicle. For that, we can use isinstance(). issubclass() is another useful function that we can use to see if a class (rather than an instance) is a subclass of another class. Add this to your code:

# class_example.py

print(f"My daily driver is a {type(daily_driver)} and my truck is a {type(truck)}")

print(f"Is my daily driver a car? {isinstance(daily_driver, Car)}")
print(f"Is my truck a Vehicle? {isinstance(truck, Vehicle)}")
print(f"Is my truck a Car? {isinstance(truck, Car)}")

print(f"Is a Truck a subclass of Vehicle? {issubclass(Truck, Vehicle)}")

Tracebacks and Exceptions

Syntax Errors

Let’s get more comfortable with exceptions. First, you’ve probably seen this one already: The IndentationError.

>>> def my_function():
... print("Hello!")
  File "<stdin>", line 2
    print("Hello!")
        ^
IndentationError: expected an indented block

Notice that we started a new function scope with the def keyword, but didn’t indent the next line of the function, the print() argument.

You’ve probably also seen the more general SyntaxError. This one’s probably obvious - something is misspelled, or the syntax is otherwise wrong. Python gives us a helpful little caret ^ under the earliest point where the error was detected, however you’ll have to learn to read this with a critical eye as sometimes the actual mistake precedes the invalid syntax. For example:

>>> a = [4,
... x = 5
  File "<stdin>", line 2
    x = 5
      ^
SyntaxError: invalid syntax

Here, the invalid syntax is x = 5, because assignment statements aren’t valid list elements, however the actual error is the missing right bracket ] on the line above.

Common Exceptions

You’ll get plenty of practice triggering syntax errors on your own. Let’s practice triggering some exceptions. Type this perfectly valid code into your REPL and see what happens:

>>> a = 1 / 0

Of course, you’ll get a divide-by-zero error, or as Python calls it, ZeroDivisionError. Some other common errors are TypeError when trying to perform an action on two unrelated types, KeyError when trying to access a dictionary key that doesn’t exist, and AttributeError when trying to access a variable or call a function that doesn’t exist on an object.

>>> 2 + "3"

>>> my_dict = {"hello": "world"}
>>> my_dict["foo"]

>>> my_dict.append("foo")

Raising our own Exceptions

Making our own Exceptions is cheap and easy, and useful for keeping track of various error states that are specific to your application. Simply inherit from the general Exception class:

>>> class MyException(Exception):
...     pass
...
>>> raise MyException()

It’s also sometimes helpful to change the default behavior for your custom Exceptions. In this case, you can simply provide your own __init__() method inside your Exception subclass:

>>> class MyException(Exception):
...     def __init__(self, message):
...             new_message = f"!!!ERROR!!! {message}"
...             super().__init__(new_message)
...
>>> raise MyException("Something went wrong!")

try, except

In Python, the “try-catch” statements use try and except. As we discussed, try is the code that could possibly throw an Exception, and except is the code that runs if the error is raised. Practice catching a KeyError by trying to access a fake dictionary key:

>>> try:
...     my_dict = {"hello": "world"}
...     print(my_dict["foo"])
... except KeyError:
...     print("Oh no! That key doesn't exist")
...

Let’s add in catching the specific KeyError object so that we can access it during the except block:

>>> try:
...     my_dict = {"hello": "world"}
...     print(my_dict["foo"])
... except KeyError as key_error:
...     print(f"Oh no! The key {key_error} doesn't exist!")
...

Re-Raising

Sometimes it’s helpful to catch an error, perform an action, and then pass the error on rather than swallowing it. This is useful when, for example, something goes wrong deep inside your code and you need to perform a special action, but also let code further up the chain know that something is wrong and the program can’t continue. Let’s divide one number by other, decrementing until we hit zero. Catch that error and immediately raise a RuntimeError:

>>> while True:
...     for divisor in range(5, -1, -1):
...         try:
...             quotient = 10 / divisor
...             print(f"10 / {divisor} = {quotient}")
...         except ZeroDivisionError:
...             print("Oops! We tried to divide by zero!")
...             raise RuntimeError
...

Solutions

classes and self

Here's what you should have seen on your command line:

class Variables

Here's what you should have seen on your command line:

Inheritance

Here's what you should have seen on your command line:

type, isinstance, and issubclass

Here's what you should have seen on your command line:

Tracebacks and Exceptions

Common Exceptions

Here's what you should have seen in your REPL:

Raising our own Exceptions

Here's what you should have seen in your REPL:

try, except

Here's what you should have seen in your REPL:

Re-Raising

Here's what you should have seen in your REPL: