Previous | Tutorial index | Next

📘 TUTORIAL 3: CREATING AND USING OBJECTS AND INSTANCES

Learning Objective

Learn how to create objects from classes and interact with them.

3.1 What Does It Mean to "Instantiate" a Class?

Instantiation is the process of creating a specific, concrete object (instance) from a class blueprint. When you instantiate a class, you are:

  1. Allocating memory for the new object.
  2. Calling __init__ to initialize the object's state.
  3. Returning a reference to the new object, which you can store in a variable.

The Syntax

To create an instance, you call the class name as if it were a function, passing any required arguments that __init__ expects.

class Dog: def __init__(self, name, age): self.name = name self.age = age # Instantiating the Dog class my_dog = Dog("Frieda", 5)

What happens behind the scenes:

  1. Python creates a new, empty object in memory.
  2. Python calls Dog.__init__(my_dog, "Frieda", 5) — note that my_dog is automatically passed as self.
  3. __init__ assigns "Frieda" to my_dog.name and 5 to my_dog.age.
  4. The variable my_dog now holds a reference to this new object.

Terminology

3.2 Creating Multiple Instances

One of the most powerful features of classes is that you can create as many independent instances as you need. Each instance has its own separate copy of the instance attributes.

class Dog: def __init__(self, name, age): self.name = name self.age = age def speak(self): return f"{self.name} says woof!" # Creating multiple independent instances dog1 = Dog("Frieda", 5) dog2 = Dog("Rex", 3) dog3 = Dog("Buddy", 7) print(dog1.name) # Frieda print(dog2.name) # Rex print(dog3.name) # Buddy

Key Insight: dog1, dog2, and dog3 are completely separate objects in memory. Changing dog1.age does not affect dog2.age or dog3.age. This independence is crucial for modelling real‑world systems where each entity has its own state.

Memory Visualization

Memory Address 1000 (dog1): [name="Frieda", age=5] Memory Address 2000 (dog2): [name="Rex", age=3] Memory Address 3000 (dog3): [name="Buddy", age=7]

Each object lives at a different memory location and maintains its own data.

3.3 Accessing Attributes Using Dot Notation

Once you have an instance, you can access its attributes using dot notation: object.attribute_name.

class Book: def __init__(self, title, author, pages): self.title = title self.author = author self.pages = pages self.current_page = 1 # Create an instance my_book = Book("1984", "George Orwell", 328) # Accessing attributes print(my_book.title) # 1984 print(my_book.author) # George Orwell print(my_book.pages) # 328 print(my_book.current_page) # 1

Reading vs. Writing Attributes

Attribute Access and Encapsulation

Remember the encapsulation principle from Tutorial 1? In Python, all attributes are technically "public" by default—you can access and modify them directly from outside the class. Later, in Tutorial 5, you'll learn how to use naming conventions (_ and __) to signal which attributes should not be accessed directly.

3.4 Modifying Attributes

Instance attributes are mutable (unless they are immutable types like strings or integers, in which case you assign new values). You can modify them at any time.

Direct Modification

class Car: def __init__(self, model, year): self.model = model self.year = year self.mileage = 0 my_car = Car("Tesla Model 3", 2023) print(my_car.mileage) # 0 # Modifying an attribute directly my_car.mileage = 15000 print(my_car.mileage) # 15000 # Modifying using arithmetic my_car.year += 1 # Now 2024

Modifying Attributes Through Methods

While direct modification is allowed, it's often better to use methods to modify attributes. This allows you to add validation and maintain encapsulation.

class BankAccount: def __init__(self, owner, balance=0): self.owner = owner self.balance = balance def deposit(self, amount): if amount <= 0: raise ValueError("Deposit amount must be positive") self.balance += amount return self.balance def withdraw(self, amount): if amount <= 0: raise ValueError("Withdrawal amount must be positive") if amount > self.balance: raise ValueError("Insufficient funds") self.balance -= amount return self.balance account = BankAccount("Alice", 1000) # Using methods is safer than direct modification account.deposit(500) # balance becomes 1500 account.withdraw(200) # balance becomes 1300 # Direct modification bypasses validation (not recommended!) account.balance = 9999999 # This would work but is dangerous!

Best Practice: Use methods to modify attributes when you need to enforce rules or trigger side effects. Direct modification is acceptable for simple, "dumb" data containers.

3.5 Calling Instance Methods

Instance methods are functions defined inside a class that operate on instances. They are called using dot notation: object.method_name(arguments).

class Dog: def __init__(self, name, age): self.name = name self.age = age self.energy = 100 def speak(self): return f"{self.name} says woof!" def play(self, minutes): self.energy -= minutes * 2 if self.energy < 0: self.energy = 0 return f"{self.name} played for {minutes} min. Energy: {self.energy}" def birthday(self): self.age += 1 return f"Happy birthday, {self.name}! Now {self.age} years old." # Create an instance fido = Dog("Fido", 3) # Call methods print(fido.speak()) # Fido says woof! print(fido.play(15)) # Fido played for 15 min. Energy: 70 print(fido.birthday()) # Happy birthday, Fido! Now 4 years old.

Important: Don't Forget the Parentheses

# Correct: calling the method result = fido.speak() # "Fido says woof!" # Incorrect: referencing the method object result = fido.speak # <bound method Dog.speak of <__main__.Dog object at 0x...>>

Without parentheses, you get a bound method object, not the result of the method. This is a very common beginner mistake.

Methods with Return Values vs. Side Effects

3.6 The Independent Nature of Instances

Each instance is completely independent. They share the same class definition (the blueprint), but their attribute values are stored separately.

Demonstration of Independence

class Student: def __init__(self, name, grade): self.name = name self.grade = grade def improve_grade(self, amount): self.grade += amount return self.grade # Create two independent students alice = Student("Alice", 85) bob = Student("Bob", 92) # Alice improves her grade alice.improve_grade(5) # Alice's grade: 90 print(alice.grade) # 90 print(bob.grade) # 92 (unchanged!) # Bob improves his grade differently bob.improve_grade(3) # Bob's grade: 95 print(alice.grade) # 90 (still unchanged!) print(bob.grade) # 95

Why Independence Matters

3.7 The __str__ and __repr__ Dunder Methods (Preview)

Although you'll cover dunder methods in detail in Tutorial 8, it's useful to briefly introduce __str__ here because it makes working with objects much more pleasant.

class Dog: def __init__(self, name, age): self.name = name self.age = age def __str__(self): return f"Dog(name={self.name}, age={self.age})" def __repr__(self): return f"Dog('{self.name}', {self.age})" fido = Dog("Fido", 3) print(fido) # Dog(name=Fido, age=3) ← uses __str__ print(repr(fido)) # Dog('Fido', 3) ← uses __repr__

Without these methods, print(fido) would show something unhelpful like <__main__.Dog object at 0x7f8a1c0b4a90>.

3.8 Practical Example: A Complete System with Multiple Objects

Let's build a small library system that demonstrates everything we've covered.

class Book: """A simple model of a library book.""" def __init__(self, title, author, isbn): self.title = title self.author = author self.isbn = isbn self.is_checked_out = False self.current_borrower = None def check_out(self, borrower_name): if self.is_checked_out: return f"Sorry, '{self.title}' is already checked out." self.is_checked_out = True self.current_borrower = borrower_name return f"'{self.title}' checked out to {borrower_name}." def return_book(self): if not self.is_checked_out: return f"'{self.title}' is already in the library." self.is_checked_out = False borrower = self.current_borrower self.current_borrower = None return f"'{self.title}' returned by {borrower}." def __str__(self): status = "Checked out" if self.is_checked_out else "Available" return f"{self.title} by {self.author} (ISBN: {self.isbn}) - {status}" # --- Creating multiple book instances --- book1 = Book("1984", "George Orwell", "978-0-452-28423-4") book2 = Book("To Kill a Mockingbird", "Harper Lee", "978-0-06-112008-4") book3 = Book("The Great Gatsby", "F. Scott Fitzgerald", "978-0-7432-7356-5") # --- Interacting with books --- print(book1) # 1984 by George Orwell (ISBN: 978-0-452-28423-4) - Available # Check out a book print(book1.check_out("Alice")) # '1984' checked out to Alice. print(book1) # 1984 ... - Checked out # Try to check out the same book again print(book1.check_out("Bob")) # Sorry, '1984' is already checked out. # Return the book print(book1.return_book()) # '1984' returned by Alice. print(book1) # 1984 ... - Available # Check out to a different person print(book1.check_out("Charlie")) # '1984' checked out to Charlie. # Book2 remains available (independence!) print(book2) # To Kill a Mockingbird ... - Available

Key observations:

3.9 Common Pitfalls When Working with Objects

Pitfall Explanation How to Avoid
Forgetting parentheses when calling a method book.check_out returns a method object, not the result. Always use book.check_out("Alice").
Modifying an attribute directly instead of using a method book.is_checked_out = False bypasses any logic in return_book(). Use the provided methods to maintain encapsulation.
Assuming objects are copied when assigned book2 = book1 makes book2 and book1 refer to the same object. Use copy or create a new instance if you need a separate object.
Printing an object without __str__ You get an unhelpful memory address. Define __str__ for user‑friendly printing.
Mutating default arguments in methods def __init__(self, items=[]): creates a shared list. Use None as default: def __init__(self, items=None):
Confusing class attributes with instance attributes Modifying a class attribute affects all instances. Use self.attribute for instance‑specific data.

📝 Quiz 3: Instantiating and Working with Objects

Answer the following questions to check your understanding.

1. What is the correct syntax to create an instance of a class named Car that has an __init__ method expecting model and year?

Answer(B) `my_car = Car("Tesla", 2023)`

2. How do you access the model attribute of a Car instance stored in variable my_car?

Answer(C) `my_car.model`

3. Consider the following code:

class Player: def __init__(self, name): self.name = name self.score = 0 p1 = Player("Alice") p2 = Player("Bob") p1.score = 100

What is p2.score?

Answer(B) `0`

4. What does the following code print?

class Greeter: def __init__(self, name): self.name = name def greet(self): return f"Hello, {self.name}!" g = Greeter("World") print(g.greet)
Answer(B) `` (missing parentheses!)

5. (True/False) If you create two instances of the same class, they share the same memory location.

Answer(B) False – they are independent.

6. Which of the following is a valid way to modify the age attribute of an instance dog?

Answer(D) All of the above

7. What will be the output of the following code?

class Counter: def __init__(self): self.count = 0 def increment(self): self.count += 1 c1 = Counter() c2 = Counter() c1.increment() c1.increment() c2.increment() print(c1.count, c2.count)
Answer(A) `2 1`

8. What does the phrase "instantiating a class" mean?

Answer(B) Creating a new instance of the class.

đŸ§Ș Exercise 3: Building and Using Objects

Part A: Create an Employee Class
Define a class called Employee with:

Part B: Instantiate and Interact
Create three employee objects (e.g., for a tech company). Perform these operations:

  1. Display all employees.
  2. Promote one employee.
  3. Make another employee "work a year" (call work_year()).
  4. Change an employee's salary directly (to demonstrate direct attribute modification).
  5. Print the updated display for all three employees.

Part C: Method Call Mistakes
Write a small script that demonstrates the following mistakes and then fix them:

  1. Calling a method without parentheses.
  2. Forgetting to include self in a method definition (this will raise an error).
  3. Trying to access an attribute that was never assigned in __init__.

Submit your corrected code with comments explaining each fix.


🏠 Homework 3: Building an Inventory Management System

Task: You are building a simple inventory management system for a small store. Create a complete Python script that implements an InventoryItem class with the following detailed specifications.

Class Requirements

Attributes (set in __init__):

Instance Methods:

  1. sell(quantity): Reduces quantity_in_stock by quantity. If quantity is negative or exceeds current stock, raise a ValueError with an appropriate message. Return the new stock level.

  2. restock(quantity): Increases quantity_in_stock by quantity. Add a tuple (current_datetime, quantity) to restock_history. If quantity is not positive, raise a ValueError. Return the new stock level.

  3. is_low_stock(): Returns True if quantity_in_stock is less than or equal to reorder_level, otherwise False.

  4. total_value(): Returns the total monetary value of the stock (price * quantity_in_stock).

  5. display(): Returns a formatted string like:

    Item #101: Laptop (Electronics) Price: $999.99 | Stock: 12 units Reorder Level: 5 | Low Stock: No Total Value: $11,999.88 Last Restock: 2026-08-11 14:30:22 (5 units)

    Hint: You can simulate timestamps with from datetime import datetime and use datetime.now().

Homework Submission Requirements

Part 1: Code
Write the complete class definition with all methods. Include:

Part 2: Testing Script
In the same file, create a main script that does the following:

  1. Create four inventory items from different categories.
  2. Perform these operations in sequence:
  3. Print a complete inventory report by calling display() on all four items.
  4. Print a list of all items that are low on stock using is_low_stock().

Part 3: Written Reflection
Answer these questions in a comment block at the top of your script:

  1. In the sell() method, why is it important to validate that the quantity is not negative and does not exceed the stock? What could happen if you omitted this validation?
  2. Why are item1, item2, item3, and item4 completely independent even though they were created from the same InventoryItem class?
  3. The restock_history attribute uses a list of tuples. Why is using a tuple appropriate for a restock event? (Hint: think about immutability.)
  4. If we later added a discount attribute and a apply_discount() method, what principle of OOP would that demonstrate?
Sample Answers (Part 3) 1. Validation prevents the stock from becoming negative or from being sold when there are not enough items. Without validation, selling more than the available stock would lead to negative quantities, which is invalid for inventory and would cause incorrect total value calculations and ordering decisions. 2. Each instance gets its own copy of the instance attributes (like `quantity_in_stock` and `restock_history`). When we create multiple objects, they reside at different memory locations and do not share these attributes, so changing one does not affect the others. 3. Tuples are immutable, which is appropriate for a restock event because the timestamp and quantity should not be modified once recorded. Using a tuple ensures that the history entry remains fixed, preserving the audit trail. 4. Adding a `discount` attribute and a `apply_discount()` method would demonstrate the **Encapsulation** principle, as the discount logic would be bundled with the data it operates on, and the method would provide controlled access to modifying the discount.

📚 Additional Resources for Self‑Study

  1. Real Python: Python Classes and Objects (Creating Objects) – Excellent coverage of the instantiation process.
  2. Programiz: Python Objects and Classes – Interactive examples with diagrams.
  3. introcs: A First Example of Class Instances – Academic perspective with clear explanations.
  4. Python Official Docs: 9.3. Instance Objects – The definitive reference.

✅ Summary Checklist for Tutorial 3

Before moving to Tutorial 4 (Inheritance), ensure you can confidently say YES to the following:

Ready for the next tutorial? In Tutorial 4, you will explore inheritance and the super() function—how to create class hierarchies, reuse code, and build relationships between classes.

Previous | Tutorial index | Next