Skip to content

Python Classes/Objects

Python is fundamentally an object-oriented language. Creating and using classes and objects is a core part of programming effectively in Python. This section introduces the key concepts of Python’s OOP support.

If you’re new to Object-Oriented Programming (OOP), understanding concepts like classes, objects, inheritance, encapsulation, and polymorphism is crucial. Consider reviewing introductory OOP materials.

  • Class: A blueprint or template for creating objects. Defines attributes (data) and methods (functions) that the objects created from the class will have.
  • Object (Instance): A specific realization of a class. If Dog is a class, then a specific dog like my_dog = Dog() is an object or instance of the Dog class.
  • Attribute: A variable associated with a class or an object. * Class Variable: Shared by all instances of a class. Defined within the class but outside any methods. * Instance Variable: Belongs to a specific instance/object. Typically defined within the __init__ method using self.variable_name.
  • Method: A function defined within a class. Operates on objects (instances) of that class. The first parameter of an instance method is conventionally named self, representing the instance itself.
  • __init__ Method (Initializer/Constructor): A special method called automatically when a new object (instance) of a class is created. Used to initialize the object’s attributes.
  • self: A conventional name for the first parameter of instance methods, representing the instance on which the method is called.
  • Inheritance: A mechanism where a new class (child/subclass) derives attributes and methods from an existing class (parent/superclass). Promotes code reuse.
  • Encapsulation: Bundling data (attributes) and methods that operate on the data within a single unit (class). Often involves controlling access to internal details (e.g., using private attributes).
  • Polymorphism: The ability of objects of different classes to respond to the same method call in a way specific to their class type (‘many forms’).

Use the class keyword followed by the class name (typically CamelCase) and a colon. The indented block contains the class definition.

class ClassName:
"""Optional docstring describing the class."""
# Class body: attributes and methods
pass # Use 'pass' for an empty class body
class Dog:
"""A simple representation of a dog."""
# Class variable (shared by all Dog instances)
species = "Canis familiaris"
def __init__(self, name, age):
"""Initialize the dog's name and age."""
# Instance variables (unique to each instance)
self.name = name
self.age = age
print(f"Dog named {self.name} created!")
# Instance method
def bark(self):
"""Simulate the dog barking."""
print(f"{self.name} says: Woof!")
# Another instance method
def describe(self):
"""Describe the dog."""
print(f"{self.name} is {self.age} years old.")

Key points:

To create an object (instance) of a class, call the class name as if it were a function, passing any arguments required by the __init__ method (excluding self).

# Create two Dog objects
my_dog = Dog("Buddy", 3)
your_dog = Dog("Lucy", 5)

Output from __init__:

Dog named Buddy created!
Dog named Lucy created!

Use the dot operator (.) to access an object’s attributes (instance variables) and call its methods.

# Access instance variables
print(f"{my_dog.name} is {my_dog.age} years old.")
# Call instance methods
my_dog.bark()
your_dog.describe()
# Access class variable (can be accessed via instance or class)
print(f"Buddy belongs to the species: {my_dog.species}")
print(f"All dogs belong to: {Dog.species}")

Output:

Buddy is 3 years old.
Buddy says: Woof!
Lucy is 5 years old.
Buddy belongs to the species: Canis familiaris
All dogs belong to: Canis familiaris

You can modify instance attributes directly (if they are not intended to be private). You can also add new attributes to specific instances dynamically.

my_dog.age = 4 # Modify existing attribute
my_dog.breed = "Golden Retriever" # Add a new attribute to my_dog only
print(f"Buddy's updated age: {my_dog.age}")
print(f"Buddy's breed: {my_dog.breed}")
# your_dog does not have the 'breed' attribute initially
# print(your_dog.breed) # Would raise AttributeError
# Modifying class variables affects all instances
Dog.species = "A good doggo"
print(f"Lucy's species now: {your_dog.species}")

Built-in Attribute Functions:

  • getattr(obj, name[, default]): Accesses the attribute name of obj. Returns default if specified and attribute doesn’t exist, otherwise raises AttributeError.
  • hasattr(obj, name): Returns True if obj has an attribute name, False otherwise.
  • setattr(obj, name, value): Sets the attribute name on obj to value. Creates the attribute if it doesn’t exist.
  • delattr(obj, name): Deletes the attribute name from obj.

Python classes have several built-in attributes, accessible via dot notation:

  • ClassName.__doc__: The class’s documentation string (or None).
  • ClassName.__name__: The class name.
  • ClassName.__module__: The name of the module where the class was defined.
  • ClassName.__bases__: A tuple containing the parent classes (base classes).
  • ClassName.__dict__: A dictionary containing the class’s namespace (class variables, methods, etc.).
  • instance.__class__: The class to which an instance belongs.
print(f"Dog.__doc__: {Dog.__doc__}")
print(f"Dog.__name__: {Dog.__name__}")
print(f"Dog.__module__: {Dog.__module__}")
print(f"Dog.__bases__: {Dog.__bases__}") # Empty tuple if no explicit inheritance
# print(f"Dog.__dict__: {Dog.__dict__}") # Shows class attributes/methods
print(f"my_dog.__class__: {my_dog.__class__}")

Garbage Collection and Destructors (__del__)

Section titled “Garbage Collection and Destructors (__del__)”

Python automatically manages memory using garbage collection, primarily based on reference counting. When an object’s reference count drops to zero (no variables point to it), Python reclaims its memory.

You can define a special method __del__(self), called a destructor, which Python attempts to call just before an object is garbage collected. It’s used for cleanup tasks like closing files or network connections associated with the object.

Caution: Relying on __del__ can be tricky due to the timing of garbage collection and potential issues with circular references. It’s often better to use context managers (with statement) for resource management.

class FileHandler:
def __init__(self, filename):
print(f"Opening {filename}")
self.filename = filename
# self.file = open(filename, 'w') # Example resource
def __del__(self):
# Cleanup actions
print(f"Destructor called for {self.filename}. Cleaning up.")
# if hasattr(self, 'file') and not self.file.closed:
# self.file.close()
fh = FileHandler("my_data.txt")
# Sometime later, when 'fh' is no longer referenced...
del fh # Manually trigger reference count drop
# ... garbage collection eventually calls __del__

Inheritance allows a class (child/subclass) to inherit attributes and methods from another class (parent/superclass). Syntax: class ChildClass(ParentClass):

class Animal:
def __init__(self, name):
self.name = name
print("Animal created")
def speak(self):
print("Generic animal sound")
def eat(self):
print(f"{self.name} is eating.")
# Cat inherits from Animal
class Cat(Animal):
def __init__(self, name, breed):
print("Cat constructor called")
# Call the parent class's __init__ method
super().__init__(name)
self.breed = breed
# Override the speak method
def speak(self):
print(f"{self.name} says: Meow!")
# Add a new method specific to Cat
def purr(self):
print(f"{self.name} is purring.")
# Create an instance of the child class
my_cat = Cat("Whiskers", "Siamese")
my_cat.eat() # Inherited method from Animal
my_cat.speak() # Overridden method in Cat
my_cat.purr() # Method specific to Cat
print(f"Breed: {my_cat.breed}")

Output:

Cat constructor called
Animal created
Whiskers is eating.
Whiskers says: Meow!
Whiskers is purring.
Breed: Siamese

super().__init__(...) is used to call the parent class’s initializer.

Multiple Inheritance: A class can inherit from multiple parents: class Child(Parent1, Parent2):. Python uses Method Resolution Order (MRO) to determine which parent method to call in case of conflicts (C3 linearization algorithm).

A child class can provide its own implementation of a method inherited from a parent class. This is called overriding. See the speak method in the Cat example above.

Python classes can define special methods with double underscores (__methodname__) that allow objects to interact with built-in Python operations (like addition, printing, comparison).

MethodOperator/FunctionDescription
__init__(self, ...)Object creationInitializer
__str__(self)str(), print()Informal, human-readable string representation
__repr__(self)repr()Official, unambiguous string representation (often code to recreate object)
__len__(self)len()Length of the object
__add__(self, other)+Addition
__eq__(self, other)==Equality comparison
__lt__(self, other)<Less than comparison
__getitem__(self, key)obj[key]Access item by key (indexing/lookup)
__call__(self, ...)obj(...)Make object callable like a function
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
def __add__(self, other):
"""Defines vector addition using the + operator."""
if isinstance(other, Vector):
return Vector(self.x + other.x, self.y + other.y)
return NotImplemented # Indicate operation not supported
def __str__(self):
"""Defines how the vector is printed."""
return f"Vector({self.x}, {self.y})"
def __repr__(self):
"""Defines the 'official' representation."""
return f"Vector(x={self.x}, y={self.y})"
v1 = Vector(2, 3)
v2 = Vector(5, -1)
v3 = v1 + v2 # Uses the __add__ method
print(v1) # Uses __str__
print(v3) # Uses __str__
print(repr(v3)) # Uses __repr__

Output:

Vector(2, 3)
Vector(7, 2)
Vector(x=7, y=2)

Python doesn’t have strict ‘private’ attributes like some languages. However, by prefixing an attribute name with two underscores (e.g., __secret), Python performs name mangling. It changes the attribute name internally to _ClassName__attributeName. This makes it harder (but not impossible) to access accidentally from outside the class, preventing naming conflicts in subclasses.

class MySecret:
def __init__(self):
self.public_var = 10
self.__secret_var = 20 # Name mangled
def get_secret(self):
return self.__secret_var
secret_obj = MySecret()
print(secret_obj.public_var) # Accessing public is fine
# print(secret_obj.__secret_var) # AttributeError!
# Access via mangled name (discouraged unless necessary)
print(secret_obj._MySecret__secret_var)
print(secret_obj.get_secret()) # Access via method is preferred

Output:

10
20
20

Conventionally, a single underscore prefix (_variable) signals that an attribute or method is intended for internal use, but it’s not enforced by Python.

Modern Python offers features like @property for creating managed attributes (getter/setter/deleter methods disguised as simple attribute access) and @dataclass (from the dataclasses module) for reducing boilerplate code when creating classes primarily meant to store data.