Inner classes
Inner Classes in Python
A class can be defined inside another class. Such a class is called an inner class, or nested class.
Suppose we are developing a student management application. Each student has a name and an address. The address itself contains details such as the city and PIN code.
We can represent the student using a Student class and group the address-related data inside an Address class.
class Student:
class Address:
pass
Here, Student is the outer class, and Address is the inner class.
The nested definition groups Address under Student. However, creating a Student object does not automatically create an Address object. We create each object explicitly.
Creating an Inner Class Object
Let us begin with a small example:
class Student:
class Address:
def display(self):
print("This is the student's address.")
address1 = Student.Address()
address1.display()
Output:
This is the student's address.
In this statement:
address1 = Student.Address()
Python accesses the Address class through Student and then creates an instance of it.
We do not need a Student object first. The inner class is accessible through the outer class itself.
| Expression | Meaning |
|---|---|
| Student | The outer class |
| Student.Address | The inner class |
| Student.Address() | Creates an Address object |
| address1.display() | Calls a method on that object |
Adding Data to the Inner Class
We can define an initializer and instance variables inside an inner class, just as we do in any other class.
class Student:
class Address:
def __init__(self, city, pin_code):
self.city = city
self.pin_code = pin_code
def display(self):
print("City:", self.city)
print("PIN Code:", self.pin_code)
address1 = Student.Address("Bengaluru", "560036")
address1.display()
Output:
City: Bengaluru PIN Code: 560036
Inside Address, self refers to the Address object receiving the method call.
It does not refer to a Student object.
The PIN code is stored as a string because it is an identifier, not a number used for arithmetic.
Connecting the Student and Address Objects
Now let us give each student an address.
class Student:
def __init__(self, name, city, pin_code):
self.name = name
self.address = Student.Address(city, pin_code)
def display(self):
print("Name:", self.name)
self.address.display()
class Address:
def __init__(self, city, pin_code):
self.city = city
self.pin_code = pin_code
def display(self):
print("City:", self.city)
print("PIN Code:", self.pin_code)
student1 = Student("Rahul", "Bengaluru", "560036")
student1.display()
Output:
Name: Rahul City: Bengaluru PIN Code: 560036
Focus on this statement inside the Student initializer:
self.address = Student.Address(city, pin_code)
It performs two actions:
-
Creates an Address object using the supplied city and PIN code.
-
Stores a reference to that object in the student’s address attribute.
The student now has two instance attributes:
| Attribute | Holds |
|---|---|
| student1.name | The string "Rahul" |
| student1.address | A reference to an Address object |
When the Student method executes:
self.address.display()
it calls display() on the associated Address object.
This relationship is also an example of composition: a Student object contains a reference to an Address object.
Nesting organizes the class definitions. The assignment to self.address creates the relationship between the objects.
Accessing the Inner Object’s Attributes
We can access address details through the student:
print(student1.address.city) print(student1.address.pin_code)
Output:
Bengaluru 560036
Read this expression from left to right:
student1.address.city
-
student1 refers to the Student object.
-
student1.address refers to its Address object.
-
.city accesses the city stored in that Address object.
We can update an address attribute in the same way:
student1.address.city = "Mysuru" student1.address.pin_code = "570001" student1.display()
Output:
Name: Rahul City: Mysuru PIN Code: 570001
Creating Multiple Students
Using the same class definition, we can create students with different addresses:
student1 = Student("Rahul", "Bengaluru", "560036")
student2 = Student("Anjali", "Hyderabad", "500001")
student1.display()
print()
student2.display()
Output:
Name: Rahul City: Bengaluru PIN Code: 560036 Name: Anjali City: Hyderabad PIN Code: 500001
Each call to the Student initializer creates a new Address object.
print(student1.address is student2.address)
Output:
False
Changing the first student’s address therefore does not change the second student’s address:
student1.address.city = "Mysuru" print(student1.address.city) print(student2.address.city)
Output:
Mysuru Hyderabad
The separate addresses result from creating a new Address instance for each student. Nesting alone does not guarantee separate objects.
Understanding self in Both Classes
Both classes use the parameter name self, but it refers to a different object in each method call.
class Student:
def __init__(self, name, city):
self.name = name
self.address = Student.Address(city)
class Address:
def __init__(self, city):
self.city = city
| Method | self refers to |
|---|---|
| Student.__init__() | The Student instance being initialized |
| Address.__init__() | The Address instance being initialized |
Inside Address, writing:
self.name
would look for name on the Address object. It would not automatically access the student’s name.
An inner class instance has no automatic reference to an outer class instance.
If it needs the outer object, we must pass that object explicitly.
Passing the Outer Object to the Inner Object
Consider an Order that has an associated Invoice. The invoice needs to read the order number and total.
class Order:
def __init__(self, order_number, total):
self.order_number = order_number
self.total = total
self.invoice = Order.Invoice(self)
class Invoice:
def __init__(self, order):
self.order = order
def display(self):
print("Order Number:", self.order.order_number)
print("Total:", self.order.total)
order1 = Order("ORD101", 1500)
order1.invoice.display()
Output:
Order Number: ORD101 Total: 1500
Inside the Order initializer:
self.invoice = Order.Invoice(self)
the self passed inside the parentheses is the current Order object.
Inside the Invoice initializer:
def __init__(self, order):
self.order = order
the parameters have different roles:
| Parameter | Refers to |
|---|---|
| self | The new Invoice object |
| order | The Order object supplied by the caller |
The invoice stores that reference in self.order. It can then access:
self.order.order_number self.order.total
If the order’s total changes, the invoice reads the updated value:
order1.total = 1800 order1.invoice.display()
Output:
Order Number: ORD101 Total: 1800
For this example, the invoice reads the live order data rather than storing a separate copy.
Accessing an Outer Class Variable
An inner class can access an outer class variable by using the outer class name.
class College:
college_name = "ABC College"
class Department:
def __init__(self, department_name):
self.department_name = department_name
def display(self):
print("College:", College.college_name)
print("Department:", self.department_name)
department1 = College.Department("Computer Science")
department1.display()
Output:
College: ABC College Department: Computer Science
The explicit expression:
College.college_name
accesses the outer class attribute.
Writing just college_name inside Department.display() does not automatically search the surrounding College class. Similarly, self.college_name would look on the Department instance and its class hierarchy.
Nesting Does Not Mean Inheritance
These definitions describe different relationships:
class Student:
class Address:
pass
Here, Address is nested inside Student.
class ResearchStudent(Student):
pass
Here, ResearchStudent inherits from Student.
An inner class does not automatically inherit the outer class’s methods or attributes. Nesting also does not make the inner class private: it remains accessible as Student.Address.
Choosing between an Inner Class and a Separate Class
An inner class can be useful when its name and purpose are closely tied to one outer class.
For example:
Order.Invoice
clearly groups the invoice class under Order.
However, if addresses are used by students, employees, customers, and suppliers, a separate Address class may be more convenient:
class Address:
def __init__(self, city, pin_code):
self.city = city
self.pin_code = pin_code
class Student:
def __init__(self, name, address):
self.name = name
self.address = address
address1 = Address("Bengaluru", "560036")
student1 = Student("Rahul", address1)
print(student1.name)
print(student1.address.city)
Output:
Rahul Bengaluru
The Student object still has an Address object. Composition works with both nested and separate class definitions.
Choose nesting when grouping the class under the outer class makes the design clearer. Use a separate class when it has a broader role in the application.
Practice
Create a Computer class with an inner class named Processor.
The Computer class should:
-
Store the computer’s brand.
-
Create a Processor object and store it in self.processor.
-
Provide a display_details() method.
The Processor class should:
-
Store model and cores.
-
Provide a display() method.
Test your program with:
computer1 = Computer("Dell", "Intel Core i5", 6)
computer1.display_details()
Expected output:
Brand: Dell Processor: Intel Core i5 Cores: 6
Then create a second computer with different processor details. Update the first computer’s core count through:
computer1.processor.cores = 8
Display both computers and verify that only the first computer’s processor details changed.