Previous | Tutorial index | Next

📘 TUTORIAL 2: DEFINING CLASSES AND USING __init__

Learning Objective

Learn the exact Python syntax for defining a class and initializing objects.

2.1 The class Keyword: Your Blueprint Starts Here

In Python, you define a new class using the class keyword, followed by the class name and a colon (:).

class Dog: pass # 'pass' is a placeholder so the class isn't empty

Naming Conventions (PEP 8)

The pass Statement

Adding a Docstring (Best Practice)

Immediately after the class header, you should include a docstring (triple‑quoted string) that describes the purpose of the class.

class Dog: """A simple model of a dog.""" pass

2.2 The __init__() Method: The Initializer (Not a Constructor)

Important distinction: In many programming languages, the method that creates an object is called a "constructor." In Python, the actual constructor is __new__(), which allocates memory for the object. The __init__() method is an initializer—it runs immediately after the object is created to set its initial state.

What Does __init__() Do?

Syntax Structure

class ClassName: def __init__(self, parameter1, parameter2, ...): self.attribute1 = parameter1 self.attribute2 = parameter2

2.3 The self Parameter: The Instance's Own Identity

self is a reference to the current instance of the class. When you create an object, Python passes that object itself as the first argument to the method.

Why Do We Need self?

Naming Convention

How Python Passes self Automatically

When you call my_dog = Dog("Fido", 3), Python internally does something like this:

  1. It creates a new, empty object in memory.
  2. It calls __init__ and passes the new object as the self argument, along with "Fido" and 3.
  3. The __init__ method uses self to attach attributes to that specific object.

Critical: When you call an instance method later (like my_dog.speak()), Python automatically passes my_dog as self. You never need to pass self manually when calling a method—Python handles it for you.

2.4 Defining Instance Attributes

Inside __init__, you define instance attributes—data that belongs to each individual object.

Syntax

class Dog: def __init__(self, name, age): self.name = name # Instance attribute self.age = age # Instance attribute

Required vs. Optional Attributes

You can also set default values for attributes, making them optional during instantiation.

class Dog: def __init__(self, name, age=0, breed="Unknown"): self.name = name self.age = age # Age defaults to 0 if not provided self.breed = breed # Breed defaults to "Unknown"

Now you can create a dog with just a name: Dog("Fido") → age is 0, breed is "Unknown".

Attributes That Don't Come from Parameters

You can define attributes that are not passed as parameters. These are useful for state that starts with a fixed default.

class Book: def __init__(self, title, author): self.title = title self.author = author self.current_page = 1 # Not passed in; always starts at 1 self.is_open = False # Not passed in; always starts closed

2.5 Creating Instance Methods

An instance method is a function defined inside a class that operates on an instance of that class.

Structure

Example: Expanding the Dog Class

class Dog: def __init__(self, name, age, breed="Unknown"): self.name = name self.age = age self.breed = breed self.energy = 100 # Instance method that reads state def speak(self): return f"{self.name} says woof!" # Instance method that modifies state def play(self, minutes): self.energy -= minutes * 2 if self.energy < 0: self.energy = 0 return f"{self.name} played for {minutes} minutes. Energy is now {self.energy}." # Instance method that uses parameters def birthday(self): self.age += 1 return f"Happy birthday, {self.name}! You are now {self.age} years old."

Why Use Instance Methods?

2.6 Full Walkthrough Example: The Student Class

Let's build a complete class from scratch to solidify the concepts.

class Student: """A simple model of a university student.""" def __init__(self, name, student_id, major): """Initialize the student with required information.""" self.name = name self.student_id = student_id self.major = major self.gpa = 0.0 # Default GPA for new students self.courses = [] # Empty list to store enrolled courses def enroll(self, course_name): """Enroll the student in a course.""" if course_name not in self.courses: self.courses.append(course_name) return f"{self.name} enrolled in {course_name}" else: return f"{self.name} is already enrolled in {course_name}" def update_gpa(self, new_gpa): """Update the student's GPA with validation.""" if 0.0 <= new_gpa <= 4.0: self.gpa = new_gpa return f"GPA updated to {self.gpa}" else: raise ValueError("GPA must be between 0.0 and 4.0") def display_info(self): """Return a formatted summary of the student.""" courses_str = ", ".join(self.courses) if self.courses else "No courses" return (f"Student: {self.name} (ID: {self.student_id})\n" f"Major: {self.major}\n" f"GPA: {self.gpa}\n" f"Courses: {courses_str}") # --- Creating and using objects --- alice = Student("Alice Johnson", "S001", "Computer Science") bob = Student("Bob Smith", "S002", "Engineering") # Calling methods print(alice.enroll("CS101")) # Alice Johnson enrolled in CS101 print(alice.enroll("CS101")) # Alice Johnson is already enrolled in CS101 print(alice.enroll("MATH201")) # Alice Johnson enrolled in MATH201 alice.update_gpa(3.8) print(alice.display_info()) print(bob.display_info()) # Bob has no courses and GPA 0.0

Output (for alice.display_info()):

Student: Alice Johnson (ID: S001) Major: Computer Science GPA: 3.8 Courses: CS101, MATH201

Notice that alice and bob are completely independent—changing Alice's GPA or courses does not affect Bob.

2.7 Common Pitfalls and How to Avoid Them

Pitfall Explanation How to Avoid
Forgetting self as the first parameter def speak(): (no self). When you call dog.speak(), Python tries to pass dog as an argument but the method doesn't accept it → TypeError: speak() takes 0 positional arguments but 1 was given. Always include self as the first parameter for all instance methods and __init__.
Forgetting to assign an attribute If you write self.name = name but forget self.age = age, then dog.age will raise an AttributeError. Always assign every attribute you intend to use. Double‑check your __init__ body.
Using a mutable default value in __init__ def __init__(self, items=[]): - This creates a shared list across all instances, leading to subtle bugs. Use def __init__(self, items=None): and then self.items = items if items is not None else [].
Accessing a method incorrectly Writing dog.speak (without parentheses) returns the method object itself, not the result. Always add parentheses when you want to call the method: dog.speak().
Confusing __init__ with a constructor Thinking __init__ creates the object. It doesn't—it initializes it. For this unit, you don't need __new__. Just know that __init__ is for setting initial state.
Using self outside a method Writing self.name = "Fido" at the top level of a class (outside any method) is invalid. Only use self inside instance methods (including __init__).

📝 Quiz 2: Class Syntax and __init__

Answer the following questions to check your understanding.

1. What is the correct way to define a class in Python?

Answer(C) `class MyClass:`

2. The __init__ method in Python is best described as:

Answer(B) An initializer that sets up the object's state after creation.

3. What does the self parameter represent in an instance method?

Answer(B) The current instance of the class.

4. Consider the following code:

class Car: def __init__(self, model): self.model = model

What is model in the parameter list?

Answer(C) A parameter that gets assigned to `self.model`.

5. Which of the following will correctly create an instance of the Book class?

class Book: def __init__(self, title, author): self.title = title self.author = author
Answer(D) Both B and C are correct.

6. (True/False) You can name the first parameter of an instance method anything you like (e.g., this), but self is the convention.

Answer(A) True

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

class Cat: def __init__(self, name): self.name = name def meow(self): return self.name + " says meow!" kitty = Cat("Fluffy") print(kitty.meow)
Answer(B) `` (missing parentheses!)

8. How do you make an attribute optional (with a default value) in __init__?

Answer(A) `def __init__(self, name, age=0):`

đŸ§Ș Exercise 2: Hands‑On Class Construction

Part A: Build a Product Class
Create a class called Product with the following requirements:

Part B: Instantiate and Test
Create three product objects (e.g., a laptop, a mouse, and a monitor). Perform the following operations:

  1. Display each product's information.
  2. Sell 2 of one product.
  3. Restock another product.
  4. Print the total value of each product.
  5. Try to sell more items than are available—catch the error gracefully using a try/except block.

Part C: Reflection Question
In a comment in your code, answer: "Why does self allow the sell() method to work correctly for each product without us having to pass the product's quantity as a separate argument?"

Sample Answer `self` refers to the specific instance on which the method is called. When we call `product1.sell(2)`, Python passes `product1` as `self`, so the method accesses that instance's `quantity` attribute directly. This allows each product object to manage its own quantity independently without needing to pass it explicitly.

🏠 Homework 2: Modeling a Banking System

Task: You are building a simplified banking system. Write a complete Python script that defines a BankAccount class with the following rigorous specifications.

Class Requirements

Attributes (set in __init__):

Instance Methods:

  1. deposit(amount): Adds amount to the balance. If amount is negative or zero, raise a ValueError. Add a transaction record like "Deposited $100.00". Return the new balance.

  2. withdraw(amount): Subtracts amount from the balance. If amount is negative or greater than the current balance, raise a ValueError. Add a transaction record like "Withdrew $50.00". Return the new balance.

  3. apply_interest(): Multiplies the current balance by (1 + interest_rate) and adds a transaction record "Applied interest at X%". Return the new balance.

  4. get_balance(): Returns the current balance.

  5. get_transaction_history(): Returns the list of transaction strings.

  6. display_summary(): Returns a formatted string like:

    Account: 12345 Holder: Alice Johnson Balance: $1,250.75 Interest Rate: 2.0% Transactions: 3

Homework Submission Requirements

Part 1: Code
Write the full class definition with all methods. Make sure to include:

Part 2: Testing Script
In the same file, create a main() function (or top‑level code) that does the following:

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

  1. What would happen to the deposit() method if we forgot to include self as the first parameter? (Be specific about the error that would occur.)
  2. Why is it beneficial that the transaction_history list is defined inside __init__ rather than as a global list outside the class? (Hint: think about encapsulation and data integrity.)
  3. If we wanted to prevent other parts of the program from directly modifying balance (e.g., preventing account.balance = 99999), how could we use encapsulation to enforce that all changes go through deposit() and withdraw()? (You don't need to code it—just explain the concept.)
Sample Answers (Part 3) 1. The `deposit()` method would be missing the `self` parameter, so when we call `account.deposit(100)`, Python would try to pass `account` as the first argument to `deposit()`, but the method expects no arguments (or different arguments) and would raise a `TypeError` about wrong number of arguments. 2. Defining `transaction_history` inside `__init__` ensures that each account has its own separate transaction history. If it were a global list, all accounts would share the same list, and transactions would be mixed together, violating encapsulation and data integrity. 3. We could make `balance` a private attribute (e.g., `__balance`) and provide only the `deposit()` and `withdraw()` methods to modify it. External code cannot directly change `__balance` because of name mangling, and we would not provide any setter method. Thus, all changes must go through the designated methods.

📚 Additional Resources for Self‑Study

If you want to deepen your understanding, explore these materials:

  1. Real Python: Python Classes and Objects (Overview) – Clear explanations with examples.
  2. Python Official Docs: 9. Classes – The definitive reference.
  3. Programiz: Python Class and Objects – Beginner‑friendly with diagrams.
  4. w3schools: Python Classes/Objects – Interactive examples you can run in your browser.

✅ Summary Checklist for Tutorial 2

Before moving to Tutorial 3 (Creating and Using Objects), ensure you can confidently say YES to the following:

Ready for the next tutorial? In Tutorial 3, you will dive deeper into creating multiple objects, accessing their attributes, and calling their methods in practical scenarios.

Previous | Tutorial index | Next