Toggle Menu Icon
Working with Python

Classes

Classes are Python’s way of creating your own custom data types, used to bundle related data and functions together. Python classes provide all the standard features of Object Oriented Programming.

Basic Class Definition

Here’s a simple class example:

class Person:
    def __init__(self, name):
        self.name = name

# Create an instance
person = Person("Marcus")
person.name
# Output: Marcus

The __init__ function is the constructor for the class. It is called automatically when you create a new instance of the class. The self parameter refers to the instance itself and is used to initialize the instance variables.

Class with Default Values

Here’s an example of a class with default values:

class Person:
    def __init__(self, name="Unknown"):
        self.name = name

So when creating instances, if you do not provide a name it will set the default value.

alice = Person("Alice")
billie = Person()

print(alice.name)   # Output: Alice
print(billie.name)  # Output: Unknown

Class vs Instance Variables

There are two types of variables in classes: class variables (shared by all instances) and instance variables (unique to each instance).

class Dog:
    # Class variable - shared by all dogs
    species = "Canis lupus"

    def __init__(self, name, breed):
        # Instance variables - unique to each dog
        self.name = name
        self.breed = breed

buddy = Dog("Buddy", "Golden Retriever")
max_dog = Dog("Max", "German Shepherd")

print(buddy.species)    # Output: Canis lupus
print(max_dog.species)  # Output: Canis lupus
print(buddy.name)       # Output: Buddy
print(max_dog.name)     # Output: Max

# Changing class variable affects all instances
Dog.species = "Domestic Dog"
print(buddy.species)    # Output: Domestic Dog
print(max_dog.species)  # Output: Domestic Dog

Methods

Methods are functions that belong to a class. The first parameter is always self, which refers to the instance:

class BankAccount:
    def __init__(self, owner, balance=0):
        self.owner = owner
        self.balance = balance

    def deposit(self, amount):
        self.balance += amount
        return self.balance

    def withdraw(self, amount):
        if amount <= self.balance:
            self.balance -= amount
            return self.balance
        else:
            return "Insufficient funds"

    def get_balance(self):
        return self.balance

# Using the class
account = BankAccount("Alice", 100)
print(account.get_balance())  # Output: 100

account.deposit(50)
print(account.get_balance())  # Output: 150

account.withdraw(30)
print(account.get_balance())  # Output: 120

print(account.withdraw(200))  # Output: Insufficient funds

String Representation

If you try to print an instance of a class, by default it won’t output anything meaningful. For example,

class Book:
    def __init__(self, title, author):
        self.title = title
        self.author = author

p = Book("Cujo", "Stephen King")
print(p)
# Output: <__main__.Book object at 0x10063b200>

To show a human-readable representation, you need to define the __str__ method.

class Book:
    def __init__(self, title, author):
        self.title = title
        self.author = author

    def __str__(self):
        return f"{self.title} by {self.author}"

p = Book("Cujo", "Stephen King")
print(p)
# Output: Cujo by Stephen King

There is also __repr__ for developers and debugging, typically __repr__ is used to provide a string representation of an object that can be used to recreate the object. If __str__ is not defined, __repr__ is used as a fallback.

class Book:
    def __init__(self, title, author):
        self.title = title
        self.author = author

    def __str__(self):
        return f"{self.title} by {self.author}"

    def __repr__(self):
        return f"Book('{self.title}', '{self.author}')"

b = Book("Cujo", "Stephen King")
print(b)        # Output: Cujo by Stephen King
print(str(b))   # Output: Cujo by Stephen King
print(repr(b))  # Output: Book('Cujo', 'Stephen King')

r = repr(b)
nb = eval(r)    # Create new book object from output
print(nb)       # Output: Cujo by Stephen King

Summary: __str__ is for human-readable output, __repr__ is for developers and debugging.

Property Decorators

Properties let you access methods like attributes. use them for computed values or to add validation:

class Circle:
    def __init__(self, radius):
        self._radius = radius

    @property
    def radius(self):
        return self._radius

    @radius.setter
    def radius(self, value):
        if value < 0:
            raise ValueError("Radius cannot be negative")
        self._radius = value

    @property
    def area(self):
        return 3.14159 * self._radius ** 2

    @property
    def circumference(self):
        return 2 * 3.14159 * self._radius

circle = Circle(5)
print(circle.radius)        # Output: 5
print(circle.area)          # Output: 78.53975
print(circle.circumference) # Output: 31.4159

circle.radius = 10
print(circle.area)          # Output: 314.159

# circle.radius = -5  # Would raise ValueError

Class Methods and Static Methods

At times you may want to create a class to create methods that work with the class itself, not instances:

class MathUtils:
    pi = 3.14159

    @classmethod
    def circle_area(cls, radius):
        return cls.pi * radius ** 2

    @staticmethod
    def add(a, b):
        return a + b

    @staticmethod
    def is_even(number):
        return number % 2 == 0

# Class methods can be called on the class
area = MathUtils.circle_area(5)
print(area)  # Output: 78.53975

# Static methods are just regular functions grouped with the class
result = MathUtils.add(10, 20)
print(result)  # Output: 30

print(MathUtils.is_even(4))  # Output: True
print(MathUtils.is_even(7))  # Output: False

Inheritance

Inheritance lets you create new classes based on existing ones:

class Animal:
    def __init__(self, name, species):
        self.name = name
        self.species = species

    def speak(self):
        return f"{self.name} makes a sound"

    def info(self):
        return f"{self.name} is a {self.species}"

class Dog(Animal):
    def __init__(self, name, breed):
        super().__init__(name, "Dog")  # Call parent constructor
        self.breed = breed

    def speak(self):  # Override parent method
        return f"{self.name} barks!"

    def fetch(self):  # New method specific to dogs
        return f"{self.name} fetches the ball"

class Cat(Animal):
    def __init__(self, name, color):
        super().__init__(name, "Cat")
        self.color = color

    def speak(self):
        return f"{self.name} meows!"

    def climb(self):
        return f"{self.name} climbs the tree"

# Using inheritance
buddy = Dog("Buddy", "Golden Retriever")
whiskers = Cat("Whiskers", "Orange")

print(buddy.info())    # Output: Buddy is a Dog
print(buddy.speak())   # Output: Buddy barks!
print(buddy.fetch())   # Output: Buddy fetches the ball

print(whiskers.info()) # Output: Whiskers is a Cat
print(whiskers.speak()) # Output: Whiskers meows!
print(whiskers.climb()) # Output: Whiskers climbs the tree

Dunder Methods

The __str__ and __repr__ earlier are dunder methods (double underscore) that are special methods that are automatically called by Python in various contexts. There are several other dunder methods that you can define in your classes to customize their behavior.

Operators

The following operator examples will use the Point class to demonstrate.

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

Equality: Define __eq__ method to compare two objects using the == operator:

    def __eq__(self, other):
        if isinstance(other, Point):
            # return True only if both x and y are equal
            return self.x == other.x and self.y == other.y
        return False

p1 = Point(2,4)
p2 = Point(3,5)
p3 = Point(2,4)
print(p1 == p2)     # Output: False
print(p1 == p3)     # Output: True

Math Operators: Define __add__ method on a class to define what it means to add two objects together using the + operator:

    def __add__(self, other):
        if isinstance(other, Point):
            return Point(self.x + other.x, self.y + other.y)
        raise TypeError("Can only add Point objects")

p1 = Point(2,4)
p2 = Point(3,5)
print(p1 + p2)      # Output: Point(5, 9)

You can also add methods for the following math operators and symbols:

MethodSymbolDescription
__sub__-Subtraction
__mul__*Multiplication
__truediv__/Division
__floordiv__//Floor Division
__mod__%Modulus
__pow__**Exponentiation

Logic Operators: Additional logic operators are defined in the same way:

MethodSymbolDescription
__lt__<Less than
__le__<=Less than or equal to
__gt__>Greater than
__ge__>=Greater than or equal to
__ne__!=Not equal to
__and__&Logical AND
__or__|Logical OR
__invert__~Logical NOT

I don’t recommend overloading operators too much, it can be fun to do so, but it can also lead to confusion and errors if not used carefully.

Iterator

You can build your own iterator to use in a loop. To do so you must implement the __iter__ and __next__ methods.

class Doubler:
    def __init__(self, start, maxim=100):
        self.num= start / 2
        self.maxim = maxim

    def __iter__(self):
        return self

    def __next__(self):
        self.num = self.num * 2
        if self.num > self.maxim:
            raise StopIteration
        return int(self.num)

print("Doubling from 2 to 100")
for i in Doubler(2):
    print(i)

print("Doubling from 3 to 200")
for i in Doubler(3, 200):
    print(i)

Generator

For simple iterators it is often easier to use a generator function to achieve the same result. A generator is a regular functions that use the yield keyword to return a value. Each time next() is called on it, the generator will resume execution from where it left off. What makes generators easier to use is the __iter__() and __next__() methods are created automatically.

def doubler(num, maxim=100):
    while num <= maxim:
        yield int(num)
        num *= 2

print("Doubling from 2 to 100")
for i in doubler(2):
    print(i)

print("Doubling from 3 to 200")
for i in doubler(3, 200):
    print(i)

Class Summary

Classes are incredibly powerful for organizing your code and modeling real-world concepts. Start with simple classes and gradually add complexity as you need it. The key is to think about what data and behaviors naturally belong together.