Classes & Objects

Classes and Objects

Like many programming languages, Python supports object-oriented programming where classes are templates that define the data and behavior of objects, which are instances of those classes. If you’ve worked with other languages that support OOP, you’ll find Python’s support for it familiar, but you might be surprised by the syntax.

As an example, here’s a definition for a class named ChecklistItem, which represents an item in a checklist or to-do list app:

class ChecklistItem:
    """
    Represents an item in a checklist or "to-do" list.

    Checklist items have the following properties:
    - name: Name of the item to display in the list
    - priority: Can be "low", "medium", or "high".
                The default is "medium".
    - checked: Can be True (checked) or False (unchecked).
               The default is False.
    """

    # Class attribute
    PRIORITY_LEVELS = ["low", "medium", "high"]

    def __init__(self, name, checked=False, priority="medium"):
        """Initialize the item."""

        # Instance attributes
        self.name = name
        self.checked = checked
        # To prevent `priority` from being set to anything other
        # than the values defined in PRIORITY_LEVELS, the initializer
        # calls `priority`'s setter method.
        self.set_priority(priority)

    def priority_emoji(self):
        """Return the item's priority as an colored emoji."""
        icons = {
            "low"   : "🟢",
            "medium": "🟨",
            "high"  : "🔴"
        }
        return icons[self.priority]

    def set_priority(self, priority):
        """Setter for item's priority."""

        # Limit `priority` to allowed values
        if priority not in self.PRIORITY_LEVELS:
            raise ValueError("Priority must be 'low', 'medium', or 'high'")

        # If the `priority` instance attribute has not previously been defined, define it.
        # Otherwise, simply update its value
        self.priority = priority

    def __str__(self):
        """Return a user-friendly string representation of the item."""
        if self.checked:
            checkbox = "✅"
        else:
            checkbox = "⬜️"
        return f"{checkbox} {self.name} {self.priority_emoji()}({self.priority})"

    def __repr__(self):
        """Return a developer-facing string representation of the item."""
        return f"name: {self.name} / priority: {self.priority} / checked: \
          {self.checked}"

Enter the code above into a new code cell and run it. The following sections will explore the parts of the ChecklistItem class in the order in which they appear. Be sure to run the code examples in a notebook!

Defining a Class

Class definitions begin with the class keyword followed by the name of the class. By convention, classes are named using the CapWords or PascalCase convention, where the first letter of every word in the name is capitalized. All indented code under class ChecklistItem: is part of the class.

The Docstring

Like functions, classes can have docstrings, strings that act as documentation, that immediately follow their first line. Also, like functions, you can access the docstring via the class’ __doc__ property. Try it by running this in a code cell: print(ChecklistItem.__doc__).

Class Attributes

Any variable, or in this case, variable being used as a constant, defined inside the class body but outside any method is a class attribute. The value of a class attribute is shared among all class instances.

The __init__() Method

The first method in the class is __init__(), the built-in initializer method for classes. Its name is often pronounced dunder init, and the double underscores surrounding its name indicates that it’s a special method.

__init__() methods are like constructors in other object-oriented programming languages, executing automatically when a new instance of the class is created. They can take arguments to set up the instance and define its attributes.

The ChecklistItem class’ __init__() method has four parameters:

  • self: The first parameter for every instance method in a Python class is for a reference to the class instance itself. Python automatically provides the argument for this parameter, which is only named self by convention. You could rename it to this, me, or any other name, but it’s best to use self if you want other developers to understand your code.
  • name, priority, and checked: These contain the argument values passed to __init__() and are used to set the newly-created instance’s name, priority, and checked attributes.

Creating a New Instance

Creating a new instance of a class in Python is straightforward:

# Create a new checklist item, using the default
# `priority` and `checked` values
item_1 = ChecklistItem("Clean the kitchen")

# Create another checklist item, but with
# `priority` set to "high" and `checked`
# set to True
item_2 = ChecklistItem("Walk the dog", True, "high")

Remember that Python automatically provides the instance reference as the argument for the __init__() method’s self parameter.

Instance Attributes

Two of the three instance attributes of ChecklistItem instances are defined in the __init__() method by these lines:

self.name = name
self.checked = checked

Instance attributes are most often defined in a class’ __init__() method, but they can be defined in any of the class’ instance methods. In fact, any variable you define inside an instance method that begins with self. or whatever name you’re using for the instance reference is an instance attribute.

The final instance attribute is defined by calling its “setter” method to prevent it from taking an invalid value:

self.set_priority(priority)

Note that the instance is created, the priority property is defined in the set_priority() method, not in __init__(). While instance attributes are often defined in a class’ __init__() method, they can be defined in any of the class’ methods.

Reading and updating an instance’s attributes in Python is done with dot notation, like many other programming languages:

print(item_1.name)  # Clean the kitchen
item_1.checked = True
item_2.name = "Finish homework"

In Python, all instance attributes are public. While there’s no such thing as private instance attributes, there are a couple of ways to indicate that an attribute is private:

  • By convention: Some Python programmers mark an attribute that should be treated as private with a single underscore at the start of its name, for example, _name. The attribute is still accessible by code outside the class definition. The leading underscore is simply a message to programmers not to do so.
  • By name mangling: An attribute whose name begins with a double underscore is harder to access by code outside the class definition. For example, for an instance item_3 of the ChecklistItem class, an attribute named __private would be accessible within the class as __private. Outside the class, its name would be mangled to include the class name so that it would have to be accessed as item_3._ChecklistItem__private.

Defining and Calling Methods

The two methods after the __init__() method are:

  • priority_emoji(), which returns an emoji representing the item’s priority to display to the user.
  • set_priority(), which takes an argument and uses it as the new value for the item’s priority. It allows only values defined in PRIORITY_LEVELS to be used as the new value.

As with the __init__() method, Python automatically provides the instance reference as the argument for any instance method’s self parameter, including these two methods.

As you may have suspected, calling an instance’s methods in Python is done with dot notation:

print(item_1.priority_emoji())
item_2.set_priority("low")

The __str__() and __repr__() Methods

There are two more methods after priority_emoji() and set_priority(), both of which are “dunder” methods (methods whose names begin and end with double-underscores) that are built into Python classes:

  • __str__(), which returns a user-facing string describing the instance.
  • __repr__(), which returns a string representation of the object for developer use and debugging.

To see them in action, run the following in a code cell:

item_3 = ChecklistItem("Do laundry", priority="low")

print("Here's the result of __str__():")
print(item_3)

print("Here's the result of __repr__():")
item_3

Properties

Properties are a special attribute that lets you define behind-the-scenes getter and setter methods that look like directly accessing the attribute. They’re similar to C#’s, Kotlin’s and Swift’s properties, or JavaScript’s get and set methods.

In the current version of the ChecklistItem class, you read a checklist item’s priority by accessing its priority attribute but write to it by calling the set_priority() method, which also disallows invalid values.

Update ChecklistItem by turning the priority attribute into a property. This will make the syntax of reading and writing to it more consistent.

In ChecklistItem, replace set_priority() with these two methods:

@property
def priority(self):
    return self._priority

@priority.setter
def priority(self, value):
    """Setter for item's priority."""

    # Limit `priority` to allowed values
    if value not in self.PRIORITY_LEVELS:
        raise ValueError("Priority must be 'low', 'medium', or 'high'")

    # If the `_priority` instance attribute has not previously been defined, define it;
    # otherwise, simply update its value
    self._priority = value

Note that both methods are named priority(). The first one is the getter, and the second is the setter.

The first method — the getter — whose header is def priority(self): is annotated with the @property decorator, which does two things:

  • It indicates that the name of the method it’s decorating, priority, is an instance property.
  • It specifies that the method it’s decorating is the getter for that property.

This method contains a single line: return self._priority. _priority is the underlying attribute of the priority property.

The second method — the setter — whose header is def priority(self, value): is annotated with the @priority.setter decorator, which specifies that the method it’s decorating is the setter for the priority property.

With these two methods defined, reading and updating a checklist item’s priority are both done via the priority property.

However, before you can try out the updated ChecklistItem class, you’ll need to update the __init__() method so that it sets the initial value of the priority property properly:

def __init__(self, name, checked=False, priority="medium"):
    """Initialize the item."""

    # Instance attributes
    self.name = name
    self.checked = checked
    self.priority = priority # `self.priority` is the property;
                             # `priority` is the parameter.

Inheritance

Like most programming languages that support object-oriented programming, Python supports class inheritance.

Here’s the definition for DueDateChecklistItem, a subclass of ChecklistItem that includes a due date and related methods:

import datetime
from datetime import date

class DueDateChecklistItem(ChecklistItem):
    """
    A `ChecklistItem` subclass with a due date and related methods.
    """

    def __init__(self, name, checked=False, priority="medium", due_date=date.today()):
        super().__init__(name, checked, priority)
        self.due_date = due_date

    @property
    def due_date(self):
        return self._due_date

    @due_date.setter
    def due_date(self, value):
        self._due_date = value

    def is_due_today(self):
        """
        An item is due today if:
        - Today is the due date
        - The item is unchecked
        """
        return self.due_date == date.today() and not self.checked

    def is_overdue(self):
        """
        An item is overdue if:
        - Today is past the due date
        - The item is unchecked
        """
        return date.today() > self.due_date and not self.checked

    def priority_emoji(self):
        """
        Return the item's priority as an colored emoji,
        plus an emoji of a "time's up" hourglass
        if the item is overdue.
        """
        icons = {
            "low"   : "🟢",
            "medium": "🟨",
            "high"  : "🔴"
        }
        if self.is_overdue():
            additional_emoji = "⌛️"
        else:
            additional_emoji = ""
        return f"{icons[self.priority]}{additional_emoji}"

Enter the code above into a new code cell in the same notebook as ChecklistItem and run it.

item_3 = DueDateChecklistItem("Clean the garage")
print(item_3)
# The result:
⬜️ Clean the garage 🟨(medium)

Note that the first line of the __init__() method is a call to the superclass’ __init__() method. In Python, the super() function returns an object that allows access to superclass members. After the superclass has been initialized, __init__() takes care of initializing the elements specific to this class, which is the due_date property.

After the __init__() method, DueDateChecklistItem defines these methods, which add functionality to ChecklistItem:

  • The due_date property and its getter and setter.
  • The is_due_today() function, which returns True if the item’s due date is today and the item is unchecked.
  • The is_overdue() function, which returns True if today is after the item’s due date and the item is unchecked.

The final method is priority_emoji() an example of method overriding in Python. It’s pretty straightforward: any method in a subclass overrides any method with the same name in its superclass — no extra keywords like override are needed. In this case, priority_emoji() returns an additional “time’s up” hourglass emoji if the project is overdue.

See forum comments
Download course materials from Github
Previous: Functions: Deep Dive Next: Writing Python Code Demo