Classes are one of the core concepts in object-oriented programming (OOP) that act as user-defined data types and blueprints for creating program objects. Defining classes allows you to encapsulate related attributes and behaviors into a single entity. Python fully supports OOP and provides a simple, straightforward syntax for defining and instantiating classes.
This comprehensive guide will teach you everything you need to know about classes in Python. We will cover the following topics in-depth with example code snippets:
Table of Contents
Open Table of Contents
Understanding Classes and Objects
A class defines a blueprint for creating objects. It encapsulates data in the form of attributes and behaviors in the form of methods in a single structure. An object is an instance of a class created at runtime. The class is the definition, and the object is the concrete realization of that definition.
For example, consider modeling a dog using OOP concepts. The generic idea or blueprint for creating dog objects is defined in the Dog
class. This class specifies details like name, age, breed, etc. as attributes. Behaviors like barking, sitting, and running are defined as methods. At runtime, we can instantiate individual dog objects, like tom
and jerry
, based on the Dog
class blueprint. Each dog object will have its own distinct name, age, and attributes as specified during object creation.
In Python, classes provide a means of bundling data and functionality together into a single neat package which serves as a blueprint for creating copies of itself in the form of objects. This approach produces modular, reusable code through encapsulation of complex data structures and behaviors into a single cohesive class interface.
Class Definitions Syntax
In Python, class definitions begin with the class
keyword, followed by the class name and a colon. By convention, class names are written in CamelCase
. The class body containing attributes and methods is indented under the class definition header.
class ClassName:
# class body
Here is an example Dog
class definition with some attributes and a simple method:
class Dog:
# Class attribute
species = 'mammal'
# Initializer / Instance attributes
def __init__(self, name, age):
self.name = name
self.age = age
# instance method
def description(self):
return f"{self.name} is {self.age} years old"
Let’s break down the parts of this class definition:
-
Dog
is the class name -
species
is a class attribute shared by all instances of this class. Attributes at the top level of the class body are class attributes. -
The
__init__()
method is the class constructor which is called automatically whenever we create a new instance. It has theself
parameter which refers to the instance being constructed. Name and age are instance attributes created inside the constructor. -
description()
is an instance method with theself
parameter. It can access and modify instance attributes for each object the method is called on.
We will expand on each of these class components in greater detail next.
Initializing Class Attributes
Class attributes are variables defined at the top level of the class body, outside of all methods. These attributes are shared by all instances of that class. For example, in the Dog
class above, species
is a class attribute since we want all dogs to be mammals. Class attributes can be accessed using the class name or instances:
print(Dog.species) # 'mammal'
tom = Dog('Tom', 5)
print(tom.species) # 'mammal'
Instance attributes are unique for each object created from the class. They are defined inside the __init__()
constructor method and will vary for different instances. For example, name
and age
attributes in the Dog
class above will be different for every dog object we create.
We can initialize instance attributes in a few ways:
- As Parameters in the
__init__()
Method
class Dog:
def __init__(self, name, age):
self.name = name
self.age = age
- With Default Parameter Values
class Dog:
def __init__(self, name='Rex', age=2):
self.name = name
self.age = age
This allows us to create a Dog without passing any arguments, using defaults.
- By Assigning Values in the Body of
__init__()
class Dog:
def __init__(self):
self.name = 'Buddy'
self.age = 5
The key takeaway is that instance attributes must be initialized on self
inside the constructor method to be accessible on class instances.
Class vs Instance Attributes
-
Class attributes are defined at the top level of the class, outside any methods. They are shared by all instances.
-
Instance attributes are defined inside
__init__()
and are unique for each object instance created from the class.
For example:
class Dog:
species = 'mammal' # class attribute
def __init__(self, name, age):
self.name = name # instance attribute
self.age = age # instance attribute
species
is the same for any dog object- Each dog instance will have its own
name
andage
This differentiation allows attributes that are common across a class to be shared efficiently, while attributes that vary across instances are allocated separately for each object.
Accessing and Modifying Attributes
Instance attributes can be accessed and modified using dot notation on the object. For example, if we created a Dog
instance tom
:
tom = Dog('Tom', 5)
print(tom.name) # Tom
print(tom.age) # 5
tom.age = 6 # modify age
print(tom.age) # 6
Class attributes are also accessed via dot notation on the class or instance:
print(Dog.species) # mammal
print(tom.species) # mammal
To summarize, instance attributes are accessed via self
inside class methods and via the instance object in external code. Class attributes are accessed using the class name or object name from anywhere.
Adding Methods to Classes
Along with data attributes, classes can also contain methods. Methods in a class are just functions that take the instance as the first self
argument. We define class methods exactly like regular functions, but inside the class:
class Dog:
def bark(self):
print("Woof woof!")
def eat(self, food):
print(f"Ate {food}")
def sleep(self):
print("zzZ")
Now these methods can be called on any Dog
object:
tom = Dog()
tom.bark() # Woof woof!
tom.eat('bone') # Ate bone
tom.sleep() # zzZ
The self
parameter refers to the calling object, similar to this
in other languages like Java or C++. Using self.name
allows method code to dynamically access or modify attributes on the calling object.
Methods serve as the behaviors and functionality associated with each class instance. Defining class methods allows code reuse since the same methods are available across all objects of that class.
Magic or Dunder Methods
Python has a number of built-in magic methods like __init__()
(constructor), __str__()
(string representation), __len__()
(length), etc. These double-underscore methods have special meanings and allow classes to integrate with Python syntax and built-in functions.
Some commonly used magic methods are:
__init__(self)
- Constructor invoked automatically when a new object is created__str__(self)
- Returns string representation of the object, used byprint()
andstr()
__repr__(self)
- Returns unambiguous string representation often used for debugging and logging__len__(self)
- Enables use oflen()
function on the object__eq__(self, other)
- Implements == operator to check for equality
For example, we can define __str__()
and __repr__()
methods in the Dog
class:
class Dog:
# ...
def __str__(self):
return f"{self.name}, a {self.age} year old dog"
def __repr__(self):
return f"Dog('{self.name}', {self.age})"
tom = Dog('Tom', 6)
print(tom) # Tom, a 6 year old dog
print(repr(tom)) # Dog('Tom', 6)
This allows the Dog
objects to produce useful string representations when printed or logged.
There are many other dunder methods like __add__()
, __getitem__()
, etc. that enable special behaviors like operator overloading and emulate container types. Mastering their usage is key to creating feature-rich classes in Python.
Class Inheritance and Polymorphism
Inheritance allows a new child class to be defined that reuses code from the parent class. This establishes an “is-a” relationship from child to parent class. For example, we can create a Labrador
class inheriting from the Dog
parent:
class Dog:
# ...
class Labrador(Dog):
# Labrador specific implementation
Child classes inherit the attributes and methods of the parent, but can also override or extend its behavior:
class Dog:
def bark(self):
print("Woof woof!")
def eat(self, food):
print("Ate food!")
class Labrador(Dog):
def bark(self):
print("Loud woof!")
Here, Labrador
inherits methods from Dog
, but provides its own implementation for bark()
. This ability of a child class to extend or override parent methods is called polymorphism.
To check and leverage polymorphism, we can use isinstance()
and super()
:
lab = Labrador()
isinstance(lab, Dog) # True
lab.bark() # Loud woof!
# Access parent method
super().bark() # Woof woof!
Inheritance allows code reuse and establishes logical hierarchies between classes (e.g. Dog -> Animal). Polymorphism provides flexibility to override parent class behavior in child classes.
Abstract Base Classes
Sometimes, we want to define a base class solely for the purpose of polymorphism and establish a common interface that child classes must implement. Such base classes are called abstract base classes in Python.
We can designate a class abstract using the abc
module:
from abc import ABC, abstractmethod
class Animal(ABC):
@abstractmethod
def sound(self):
pass
ABC
is imported fromabc
module- Decorating a method with
@abstractmethod
marks it abstract - Abstract methods don’t need function definitions
Now we can derive concrete, non-abstract child classes from Animal
:
class Dog(Animal):
def sound(self):
print("Bark!")
class Cat(Animal):
def sound(self):
print("Meow")
Using abstract base classes ensures derived classes implement the required methods, promoting polymorphic code reuse in Python OOP.
Metaclasses
While classes define the blueprint for instance objects, metaclasses define the blueprint for classes themselves. They allow you to control class creation programmatically.
Every Python class is an instance of type
by default:
print(type(Dog)) # <class 'type'>
We can also subclass type
to define custom metaclasses. For example:
class CustomMeta(type):
@classmethod
def __prepare__(meta, name, bases):
# Custom class body prep
return {}
def __new__(meta, name, bases, classdict):
# Class creation logic
return type.__new__(meta, name, bases, classdict)
def __init__(cls, name, bases, classdict):
# Extra initialization
The complex metaclasses API allows full control over class construction, letting you modify class bodies, implement custom namespaces, apply decorators or wrappers, etc.
Metaclasses provide extremely fine-grained control over Python’s class construction mechanism. While not commonly needed, they allow implementing frameworks that can fundamentally change how classes work in Python.
Conclusion
This guide gave a comprehensive overview of defining and using classes in Python, covering:
- Class syntax and components like attributes and methods
- Special methods like constructors, string representation, and operators
- Inheritance relationships between parent and child classes
- Polymorphism and abstract base classes
- Custom metaclasses that control class creation
Here are some key takeways:
- Classes bundle data and code into reusable, modular blueprints for creating program objects
- Attributes capture data associated with each class instance
- Methods implement behaviors and functionality for objects
- Inheritance and polymorphism facilitate code reuse and abstraction
- Magic methods customize classes to work smoothly in Python
- Metaclasses allow advanced class customization and control
Equipped with these OOP concepts, you can now leverage the full power of classes in Python to write concise, flexible, and reusable code. The class paradigm is integral to Python and mastering it will enable you to model complex real-world systems with ease.