Introduction
Python's object-oriented programming (OOP) features provide powerful tools to create expressive and customized objects. Among these, the special methods __init__, __str__, and __repr__ play crucial roles in defining the behavior and representation of your Python objects.
In this tutorial, we will explore these special methods, understand their purposes, and learn how to implement them effectively in your Python classes. By the end of this lab, you will be able to create more intuitive and user-friendly Python objects.
Creating Classes with the __init__ Method
The first special method we will explore is __init__, which is called when an object is created. This method allows you to initialize the attributes of your object.
Understanding the __init__ Method
The __init__ method, also known as a constructor, is automatically called when you create a new instance of a class. Its primary purpose is to set up the initial state of the object by assigning values to attributes.
Let's start by creating a simple Python class with the __init__ method:
Open the terminal in your LabEx environment.
Navigate to the project directory:
cd ~/projectCreate a new Python file called
person.pyusing the code editor:In the editor, add the following code to
person.py:class Person: def __init__(self, name, age): self.name = name self.age = age ## Create a Person object person1 = Person("Alice", 30) ## Access the attributes print(f"Name: {person1.name}") print(f"Age: {person1.age}")Save the file.
Run the Python script:
python3 person.py
You should see the following output:
Name: Alice
Age: 30
Explanation of the __init__ Method
In the code above:
- We defined a
Personclass with an__init__method that takes three parameters:self,name, andage. - The
selfparameter refers to the instance being created and is automatically passed when an object is created. - Inside the
__init__method, we assign the values ofnameandageto the object's attributes usingself.nameandself.age. - When we create a new
Personobject withperson1 = Person("Alice", 30), the__init__method is automatically called. - We can then access the attributes using dot notation:
person1.nameandperson1.age.
Adding More Functionality
Let's enhance our Person class by adding a method to calculate the birth year based on the current year and the person's age:
Open
person.pyin the editor.Update the code to include a method for calculating the birth year:
import datetime class Person: def __init__(self, name, age): self.name = name self.age = age def birth_year(self): current_year = datetime.datetime.now().year return current_year - self.age ## Create a Person object person1 = Person("Alice", 30) ## Access the attributes and call the method print(f"Name: {person1.name}") print(f"Age: {person1.age}") print(f"Birth Year: {person1.birth_year()}")Save the file.
Run the Python script:
python3 person.py
The output should now include the calculated birth year:
Name: Alice
Age: 30
Birth Year: 1993
(The actual birth year will depend on the current year when you run the script)
Now you have created a Python class with the __init__ method to initialize object attributes and added a method to perform calculations based on those attributes.
Implementing the __str__ Method
Now that we understand how to create a class with the __init__ method, let's explore another special method called __str__. This method allows us to define how an object should be represented as a string.
Understanding the __str__ Method
The __str__ method is called when you use the str() function on an object or when you print an object using the print() function. It should return a human-readable string representation of the object.
Let's update our Person class to include a __str__ method:
Open
person.pyin the editor.Update the code to include a
__str__method:import datetime class Person: def __init__(self, name, age): self.name = name self.age = age def birth_year(self): current_year = datetime.datetime.now().year return current_year - self.age def __str__(self): return f"{self.name}, {self.age} years old" ## Create a Person object person1 = Person("Alice", 30) ## Access the attributes and call the method print(f"Name: {person1.name}") print(f"Age: {person1.age}") print(f"Birth Year: {person1.birth_year()}") ## Use the __str__ method implicitly print("\nString representation of the object:") print(person1) ## Use the __str__ method explicitly print("\nExplicit string conversion:") print(str(person1))Save the file.
Run the Python script:
python3 person.py
You should see output similar to this:
Name: Alice
Age: 30
Birth Year: 1993
String representation of the object:
Alice, 30 years old
Explicit string conversion:
Alice, 30 years old
How __str__ Works
In the code above:
- We defined a
__str__method that returns a formatted string with the person's name and age. - When we call
print(person1), Python automatically calls the__str__method to determine what to display. - We can also explicitly convert an object to a string using
str(person1), which also calls the__str__method.
What Happens Without __str__
To understand the importance of the __str__ method, let's see what happens when we don't define it:
Create a new file called
without_str.py:Add the following code:
class SimpleClass: def __init__(self, value): self.value = value ## Create an object obj = SimpleClass(42) ## Print the object print(obj)Save the file.
Run the script:
python3 without_str.py
You should see output like:
<__main__.SimpleClass object at 0x7f2d8c3e9d90>
The output is not very informative. It shows the class name and the memory address of the object, but not its content. This is why implementing a proper __str__ method is important for making your objects more user-friendly.
Practical Exercise
Let's create a new class called Book with a __str__ method:
Create a new file called
book.py:Add the following code:
class Book: def __init__(self, title, author, pages): self.title = title self.author = author self.pages = pages def __str__(self): return f'"{self.title}" by {self.author} ({self.pages} pages)' ## Create book objects book1 = Book("The Great Gatsby", "F. Scott Fitzgerald", 180) book2 = Book("To Kill a Mockingbird", "Harper Lee", 281) ## Print the books print(book1) print(book2)Save the file.
Run the script:
python3 book.py
The output should be:
"The Great Gatsby" by F. Scott Fitzgerald (180 pages)
"To Kill a Mockingbird" by Harper Lee (281 pages)
Now you understand how to use the __str__ method to create human-readable string representations of your objects.
Implementing the __repr__ Method
In addition to __str__, Python provides another special method for string representation: __repr__. While __str__ is meant to provide a human-readable representation, __repr__ is intended to provide an unambiguous representation of an object that can be used to recreate the object if possible.
Understanding the __repr__ Method
The __repr__ method is called when you use the repr() function on an object or when you display an object in an interactive session. It should return a string that, when passed to eval(), would create an equivalent object (when possible).
Let's update our Book class to include a __repr__ method:
Open
book.pyin the editor.Update the code to include a
__repr__method:class Book: def __init__(self, title, author, pages): self.title = title self.author = author self.pages = pages def __str__(self): return f'"{self.title}" by {self.author} ({self.pages} pages)' def __repr__(self): return f'Book("{self.title}", "{self.author}", {self.pages})' ## Create book objects book1 = Book("The Great Gatsby", "F. Scott Fitzgerald", 180) book2 = Book("To Kill a Mockingbird", "Harper Lee", 281) ## Print the books (uses __str__) print("String representation (using __str__):") print(book1) print(book2) ## Get the representation (uses __repr__) print("\nRepresentation (using __repr__):") print(repr(book1)) print(repr(book2))Save the file.
Run the script:
python3 book.py
You should see output like:
String representation (using __str__):
"The Great Gatsby" by F. Scott Fitzgerald (180 pages)
"To Kill a Mockingbird" by Harper Lee (281 pages)
Representation (using __repr__):
Book("The Great Gatsby", "F. Scott Fitzgerald", 180)
Book("To Kill a Mockingbird", "Harper Lee", 281)
Differences Between __str__ and __repr__
The main differences between __str__ and __repr__ are:
__str__is meant for human-readable output, while__repr__is meant for developers and debugging.- If
__str__is not defined but__repr__is, Python will use__repr__as a fallback forstr()orprint(). __repr__should ideally return a string that can recreate the object when passed toeval(), although this is not always possible or necessary.
The eval() Function with __repr__
When implemented correctly, the string returned by __repr__ can be used with eval() to recreate the object. Let's see this in action:
Create a new file called
repr_eval.py:Add the following code:
class Point: def __init__(self, x, y): self.x = x self.y = y def __str__(self): return f"Point at ({self.x}, {self.y})" def __repr__(self): return f"Point({self.x}, {self.y})" ## Create a point p1 = Point(3, 4) ## Get the repr string repr_str = repr(p1) print(f"Representation: {repr_str}") ## Use eval to recreate the object p2 = eval(repr_str) print(f"Recreated object: {p2}") ## Verify they have the same values print(f"p1.x = {p1.x}, p1.y = {p1.y}") print(f"p2.x = {p2.x}, p2.y = {p2.y}")Save the file.
Run the script:
python3 repr_eval.py
You should see output like:
Representation: Point(3, 4)
Recreated object: Point at (3, 4)
p1.x = 3, p1.y = 4
p2.x = 3, p2.y = 4
This demonstrates that we can recreate the original object using the string returned by __repr__ and the eval() function.
When to Use Each Method
- Use
__init__to set up the initial state of your objects. - Use
__str__to provide a human-readable representation for end-users. - Use
__repr__to provide a precise, unambiguous representation for developers and debugging.
Practical Exercise: A Complete Example
Let's put it all together by creating a Rectangle class with all three special methods:
Create a new file called
rectangle.py:Add the following code:
class Rectangle: def __init__(self, width, height): self.width = width self.height = height def area(self): return self.width * self.height def perimeter(self): return 2 * (self.width + self.height) def __str__(self): return f"Rectangle with width {self.width} and height {self.height}" def __repr__(self): return f"Rectangle({self.width}, {self.height})" ## Create rectangles rect1 = Rectangle(5, 10) rect2 = Rectangle(3, 7) ## Display information about the rectangles print(f"Rectangle 1: {rect1}") print(f"Area: {rect1.area()}") print(f"Perimeter: {rect1.perimeter()}") print(f"Representation: {repr(rect1)}") print("\nRectangle 2: {0}".format(rect2)) print(f"Area: {rect2.area()}") print(f"Perimeter: {rect2.perimeter()}") print(f"Representation: {repr(rect2)}") ## Recreate a rectangle using eval rect3 = eval(repr(rect1)) print(f"\nRecreated rectangle: {rect3}") print(f"Is it the same area? {rect3.area() == rect1.area()}")Save the file.
Run the script:
python3 rectangle.py
You should see output like:
Rectangle 1: Rectangle with width 5 and height 10
Area: 50
Perimeter: 30
Representation: Rectangle(5, 10)
Rectangle 2: Rectangle with width 3 and height 7
Area: 21
Perimeter: 20
Representation: Rectangle(3, 7)
Recreated rectangle: Rectangle with width 5 and height 10
Is it the same area? True
This example demonstrates how all three special methods (__init__, __str__, and __repr__) work together to create a well-designed class.
Creating a Practical Application
Now that we understand the three special methods __init__, __str__, and __repr__, let's create a more practical application that demonstrates their use in a real-world scenario. We will create a simple banking system with a BankAccount class.
Building a Bank Account Class
Create a new file called
bank_account.py:Add the following code:
class BankAccount: def __init__(self, account_number, owner_name, balance=0.0): self.account_number = account_number self.owner_name = owner_name self.balance = balance self.transactions = [] def deposit(self, amount): if amount <= 0: print("Deposit amount must be positive") return False self.balance += amount self.transactions.append(f"Deposit: +${amount:.2f}") return True def withdraw(self, amount): if amount <= 0: print("Withdrawal amount must be positive") return False if amount > self.balance: print("Insufficient funds") return False self.balance -= amount self.transactions.append(f"Withdrawal: -${amount:.2f}") return True def get_transaction_history(self): return self.transactions def __str__(self): return f"Account {self.account_number} | Owner: {self.owner_name} | Balance: ${self.balance:.2f}" def __repr__(self): return f'BankAccount("{self.account_number}", "{self.owner_name}", {self.balance})'Save the file.
Testing the Bank Account Class
Now let's test our BankAccount class:
Create a new file called
bank_test.py:Add the following code:
from bank_account import BankAccount ## Create bank accounts account1 = BankAccount("12345", "John Doe", 1000.0) account2 = BankAccount("67890", "Jane Smith", 500.0) ## Display initial account information print("Initial Account Status:") print(account1) print(account2) ## Perform transactions print("\nPerforming transactions...") ## Deposit to account1 print("\nDepositing $250 to account1:") account1.deposit(250) print(account1) ## Withdraw from account2 print("\nWithdrawing $100 from account2:") account2.withdraw(100) print(account2) ## Try to withdraw too much from account2 print("\nTrying to withdraw $1000 from account2:") account2.withdraw(1000) print(account2) ## Try to deposit a negative amount to account1 print("\nTrying to deposit -$50 to account1:") account1.deposit(-50) print(account1) ## Display transaction history print("\nTransaction history for account1:") for transaction in account1.get_transaction_history(): print(f"- {transaction}") print("\nTransaction history for account2:") for transaction in account2.get_transaction_history(): print(f"- {transaction}") ## Recreate an account using repr print("\nRecreating account1 using repr:") account1_repr = repr(account1) print(f"Representation: {account1_repr}") recreated_account = eval(account1_repr) print(f"Recreated account: {recreated_account}")Save the file.
Run the script:
python3 bank_test.py
You should see output similar to this:
Initial Account Status:
Account 12345 | Owner: John Doe | Balance: $1000.00
Account 67890 | Owner: Jane Smith | Balance: $500.00
Performing transactions...
Depositing $250 to account1:
Account 12345 | Owner: John Doe | Balance: $1250.00
Withdrawing $100 from account2:
Account 67890 | Owner: Jane Smith | Balance: $400.00
Trying to withdraw $1000 from account2:
Insufficient funds
Account 67890 | Owner: Jane Smith | Balance: $400.00
Trying to deposit -$50 to account1:
Deposit amount must be positive
Account 12345 | Owner: John Doe | Balance: $1250.00
Transaction history for account1:
- Deposit: +$250.00
Transaction history for account2:
- Withdrawal: -$100.00
Recreating account1 using repr:
Representation: BankAccount("12345", "John Doe", 1250.0)
Recreated account: Account 12345 | Owner: John Doe | Balance: $1250.00
Understanding the Implementation
In this banking application:
The
__init__method initializes a bank account with an account number, owner name, and an optional initial balance. It also creates an empty list to track transactions.The
depositandwithdrawmethods handle transactions and update the balance and transaction history.The
__str__method provides a user-friendly representation of the account, showing the account number, owner name, and current balance.The
__repr__method provides a string that can recreate the account object (although note that the transaction history is not preserved in this implementation).
This example demonstrates how these special methods can be used in a practical application to create more intuitive and user-friendly objects.
Extending the Application
As a final exercise, let's create a simple banking system that manages multiple accounts:
Create a new file called
banking_system.py:Add the following code:
from bank_account import BankAccount class BankingSystem: def __init__(self, bank_name): self.bank_name = bank_name self.accounts = {} def create_account(self, account_number, owner_name, initial_balance=0.0): if account_number in self.accounts: print(f"Account {account_number} already exists") return None account = BankAccount(account_number, owner_name, initial_balance) self.accounts[account_number] = account return account def get_account(self, account_number): return self.accounts.get(account_number) def list_accounts(self): return list(self.accounts.values()) def __str__(self): return f"{self.bank_name} - Managing {len(self.accounts)} accounts" def __repr__(self): return f'BankingSystem("{self.bank_name}")' ## Create a banking system bank = BankingSystem("Python First Bank") print(bank) ## Create some accounts bank.create_account("A001", "John Doe", 1000) bank.create_account("A002", "Jane Smith", 500) bank.create_account("A003", "Bob Johnson", 250) ## List all accounts print("\nAll accounts:") for account in bank.list_accounts(): print(account) ## Make some transactions account = bank.get_account("A001") if account: print(f"\nBefore deposit: {account}") account.deposit(500) print(f"After deposit: {account}") account = bank.get_account("A002") if account: print(f"\nBefore withdrawal: {account}") account.withdraw(200) print(f"After withdrawal: {account}") ## Try to create an existing account print("\nTrying to create an existing account:") bank.create_account("A001", "Someone Else", 300) ## Final state of the banking system print(f"\n{bank}")Save the file.
Run the script:
python3 banking_system.py
You should see output similar to:
Python First Bank - Managing 0 accounts
All accounts:
Account A001 | Owner: John Doe | Balance: $1000.00
Account A002 | Owner: Jane Smith | Balance: $500.00
Account A003 | Owner: Bob Johnson | Balance: $250.00
Before deposit: Account A001 | Owner: John Doe | Balance: $1000.00
After deposit: Account A001 | Owner: John Doe | Balance: $1500.00
Before withdrawal: Account A002 | Owner: Jane Smith | Balance: $500.00
After withdrawal: Account A002 | Owner: Jane Smith | Balance: $300.00
Trying to create an existing account:
Account A001 already exists
Python First Bank - Managing 3 accounts
This example demonstrates how to use the BankAccount class we created earlier in a more complete application. It shows how the special methods __init__, __str__, and __repr__ provide a solid foundation for creating intuitive and user-friendly classes.
Summary
In this lab, you learned about three essential special methods in Python that help you create more expressive and user-friendly objects:
__init__: The constructor method that initializes an object's attributes when it's created.__str__: A method that provides a human-readable string representation of an object, primarily for end-users.__repr__: A method that provides an unambiguous string representation of an object, primarily for developers and debugging.
You implemented these methods in several examples, from simple classes like Person and Book to more complex applications like a banking system. You also learned the differences between __str__ and __repr__ and when to use each one.
By mastering these special methods, you can create more intuitive and customized Python objects that integrate seamlessly with the language's built-in features. This knowledge forms an essential part of object-oriented programming in Python and will help you write cleaner, more maintainable code.
As you continue your Python journey, remember that there are many other special methods available that allow you to customize the behavior of your objects even further. Exploring these methods will give you even more control over how your objects behave in different contexts.



