> ## Documentation Index
> Fetch the complete documentation index at: https://genai.codewithsiva.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Classes & OOP

> Blueprint class definitions, inheritance, encapsulation, class/static methods, and properties in Python

## 1. Classes and Instances

Object-Oriented Programming (OOP) is a paradigm that groups related data (attributes) and behaviors (methods) into reusable blueprints called **classes**. An individual object built from a class is an **instance**.

A class is a blueprint for creating objects. In this example, we create a simple `Student` class with two attributes (`name` and `age`) and two methods (`introduce()` and `study()`).

```python theme={null}
# Blueprint class definition

class Student:
    # Constructor
    def __init__(self, name, age):
        self.name = name
        self.age = age

    # Method
    def study(self):
        print(f"{self.name} is studying.")

    # Method
    def introduce(self):
        print(f"My name is {self.name}.")
        print(f"I am {self.age} years old.")


# Create an object
student1 = Student("Alice", 20)

# Access attributes
print(student1.name)
print(student1.age)

# Call methods
student1.introduce()
student1.study()
```

### Output

```text theme={null}
Alice
20
My name is Alice.
I am 20 years old.
Alice is studying.
```

## Key Concepts

* **Class**: `Student`
* **Object**: `student1`
* **Attributes**: `name`, `age`
* **Methods**: `introduce()`, `study()`
* **Constructor**: `__init__()` initializes the object's attributes.

This example demonstrates:

* Defining a class using `class`
* Initializing attributes using `__init__()`
* Creating methods inside a class
* Creating an object
* Accessing object attributes
* Calling object methods

***

## 2. Attributes and Methods

Classes can hold both data and code. These are categorized into instance-level and class-level members.

### Instance Methods

Functions inside a class that operate on a specific instance of that class. They must accept `self` as the first argument.

```python theme={null}
class Account:
    def __init__(self, owner: str, balance: float):
        self.owner = owner
        self.balance = balance

    def deposit(self, amount: float):
        self.balance += amount
        return self.balance
```

### Class Attributes & Methods (`@classmethod`)

* **Class Attributes**: Variables shared by all instances of a class.
* **Class Methods**: Decorated with `@classmethod`, they accept `cls` (the class itself) instead of `self`. They are used for factory methods or modifying class-level state.

```python theme={null}
class Account:
    bank_name = "State Bank of India"  # Class attribute

    def __init__(self, owner: str, balance: float):
        self.owner = owner
        self.balance = balance

    @classmethod
    def update_bank(cls, new_name: str):
        cls.bank_name = new_name  # Modifies class-level attribute
```

### Static Methods (`@staticmethod`)

Decorated with `@staticmethod`, these do not accept `self` or `cls`. They act like normal helper functions grouped inside the class namespace.

```python theme={null}
class Account:
    @staticmethod
    def is_valid_amount(amount: float) -> bool:
        return amount > 0
```

### Type Checking (`type` vs `isinstance`)

* `type(obj)` returns the exact class/type of an object.
* `isinstance(obj, ClassName)` checks if an object is an instance of a class or any subclass (preferred for polymorphism).

```python theme={null}
class Animal: pass
class Dog(Animal): pass

buddy = Dog()

print(type(buddy) is Dog)       # True
print(type(buddy) is Animal)    # False (checks exact class)
print(isinstance(buddy, Animal)) # True (checks inheritance hierarchy)
```

***

## 3. Class Inheritance

Inheritance allows a new class (child/subclass) to inherit attributes and methods from an existing class (parent/superclass).

```python theme={null}
class Animal:
    def __init__(self, name: str):
        self.name = name

    def eat(self):
        return f"{self.name} is eating"

# Dog inherits from Animal
class Dog(Animal):
    def __init__(self, name: str, breed: str):
        super().__init__(name)  # Initialize parent attributes
        self.breed = breed

    def bark(self):
        return "Woof!"

buddy = Dog("Buddy", "Golden Retriever")
print(buddy.eat())  # Buddy is eating (inherited method)
print(buddy.bark()) # Woof!
```

### Passing Arguments Along with Class Name in Inheritance

You can pass custom configuration arguments directly inside the class declaration parentheses next to the parent class name. This is done by implementing the special class method `__init_subclass__` in the parent class.

```python theme={null}
class DatabaseModel:
    table_name: str

    # Hook called when a subclass is defined
    def __init_subclass__(cls, table: str, **kwargs):
        super().__init_subclass__(**kwargs)
        cls.table_name = table

# Passing the 'table' argument directly with the class name
class UserProfile(DatabaseModel, table="users"):
    pass

print(UserProfile.table_name)  # Output: users
```

#### What happens when this code is run?

1. **Class-level Interception**: When Python compiles and executes the class definition of `UserProfile`, it notices that `DatabaseModel` defines a `__init_subclass__` method.
2. **Hook Execution**: Instead of just creating the `UserProfile` class normally, Python automatically calls `DatabaseModel.__init_subclass__(UserProfile, table="users")`.
3. **Property Assignment**: The method receives the newly created subclass as `cls` and the keyword argument `table`. It then sets `cls.table_name = "users"`. This operates at **class-definition time** (import time), long before any instances of `UserProfile` are created.

#### Why do we need this pattern?

* **Declarative Configuration**: In ORM libraries (like SQLAlchemy or SQLModel) or API frameworks, it is common to define database tables or routes declaratively. This pattern allows subclasses to configure themselves during creation.

***

## 4. Encapsulation

Encapsulation restricts direct access to an object's internal state to prevent accidental modifications.

### Private & Protected Fields

* **Protected** (`_name`): A convention suggesting the field is for internal/subclass use. Python does not enforce this.
* **Private** (`__pin`): Triggers Name Mangling (`_ClassName__pin`), causing Python to raise an `AttributeError` if accessed directly.

```python theme={null}
class User:
    def __init__(self, username: str, pin: str):
        self.username = username    # Public
        self._status = "active"     # Protected
        self.__pin = pin            # Private
```

## 5. Properties

### Property Getters & Setters (`@property`)

Properties allow you to define methods that behave like attributes, which is useful for validating data when reading or writing a field.

```python theme={null}
class Product:
    def __init__(self, name: str, price: float):
        self.name = name
        self._price = price

    @property
    def price(self) -> float:
        """Getter method"""
        return self._price

    @price.setter
    def price(self, value: float):
        """Setter method with validation"""
        if value < 0:
            raise ValueError("Price cannot be negative")
        self._price = value

# Usage
item = Product("Laptop", 999.0)
print(item.price)  # Accesses property getter -> 999.0
item.price = 1050.0  # Accesses property setter
```

***

## 6.Abstract Classes

Python provides built-in mechanisms to inspect objects at runtime, define interfaces/abstract classes, and customize how arguments are passed during class inheritance.

### Abstract Base Classes (ABCs)

The `abc` module allows you to define abstract classes that cannot be instantiated and force subclasses to implement specific methods.

```python theme={null}
from abc import ABC, abstractmethod

class BaseRepository(ABC):
    @abstractmethod
    def save(self, data: dict) -> None:
        """Subclasses must implement this method"""
        pass

# repo = BaseRepository()  # TypeError: Can't instantiate abstract class BaseRepository

class SQLRepository(BaseRepository):
    def save(self, data: dict) -> None:
        print(f"Saving {data} to Database")
```

## 7. Introspection

### Introspection: `__dict__` and `__annotations__`

* **`__dict__`**: A dictionary containing the namespaces of an object (its attributes and values).
* **`__annotations__`**: A dictionary containing type annotations defined on the class or function.

```python theme={null}
class User:
    username: str
    email: str

    def __init__(self, username: str, email: str):
        self.username = username
        self.email = email

# Inspecting class annotations
print(User.__annotations__) 
# Output: {'username': <class 'str'>, 'email': <class 'str'>}

# Inspecting instance attributes
u = User(username="alice", email="alice@example.com")
print(u.__dict__) 
# Output: {'username': 'alice', 'email': 'alice@example.com'}
```

## Type Annotation

A type annotation only specifies the expected type of an attribute.

It **does not create or initialize** the attribute.

```python theme={null}
class Product:
    name: str
    price: float
```

Here, `name` and `price` are only type hints.

Attempting to access them through the class results in an error.

```python theme={null}
print(Product.name)
```

Output:

```text theme={null}
AttributeError: type object 'Product' has no attribute 'name'
```

***

## Instance Variables

Instance variables are created when a value is assigned to `self`.

```python theme={null}
class Product:
    name: str
    price: float

    def __init__(self, name, price):
        self.name = name
        self.price = price
```

Each object gets its own copy.

```python theme={null}
p1 = Product("Laptop", 999.99)
p2 = Product("Phone", 699.99)

print(p1.name)
print(p2.name)
```

Output:

```text theme={null}
Laptop
Phone
```

***

## Class Variables

A class variable belongs to the class and is shared by all instances.

```python theme={null}
class Product:
    category = "Electronics"
```

It can be accessed through either the class or an object.

```python theme={null}
print(Product.category)

p = Product()
print(p.category)
```

Output:

```text theme={null}
Electronics
Electronics
```

***

## Type Annotations for Class Variables

Class variables can also be annotated.

```python theme={null}
class Product:
    category: str = "Electronics"
```

The annotation documents the expected type but is **not enforced by Python**.

```python theme={null}
Product.category = 100

print(Product.category)
```

Output:

```text theme={null}
100
```

***

## Default Values

A default value provides an initial value for an instance attribute.

For example, in Pydantic:

```python theme={null}
from pydantic import BaseModel

class User(BaseModel):
    name: str
    country: str = "India"
```

```python theme={null}
user = User(name="Siva")

print(user.country)
```

Output:

```text theme={null}
India
```

Each object receives its own default value unless another value is provided.

***

## The Ambiguity

Consider this declaration:

```python theme={null}
class Product:
    category: str = "Electronics"
```

Its meaning depends on the framework.

### In Plain Python

```python theme={null}
class Product:
    category: str = "Electronics"
```

`category` is a **class variable**.

### In Pydantic

```python theme={null}
from pydantic import BaseModel

class Product(BaseModel):
    category: str = "Electronics"
```

`category` becomes an **instance field with a default value**.

This difference exists because Pydantic interprets annotated attributes as model fields.

***

## Using `ClassVar`

To explicitly indicate that an attribute is a class variable, use `ClassVar`.

```python theme={null}
from typing import ClassVar

class Product:
    category: ClassVar[str] = "Electronics"
```

In Pydantic:

```python theme={null}
from typing import ClassVar
from pydantic import BaseModel

class Product(BaseModel):
    category: ClassVar[str] = "Electronics"
    name: str
```

Here:

* `category` is a class variable.
* `name` is a model field.

Pydantic ignores `ClassVar` attributes when creating the model.

***

## Comparison

| Declaration                    | Plain Python                 | Pydantic                                                               |
| ------------------------------ | ---------------------------- | ---------------------------------------------------------------------- |
| `name: str`                    | Type annotation only         | Required model field                                                   |
| `name: str = "Siva"`           | Class variable               | Model field with a default value                                       |
| `name: ClassVar[str] = "Siva"` | Class variable               | Class variable (ignored as a model field)                              |
| `self.name = value`            | Creates an instance variable | Not applicable inside `BaseModel` (Pydantic generates the constructor) |

***

## Type Annotation vs Class Variable

| Type Annotation                                     | Class Variable                                           |
| --------------------------------------------------- | -------------------------------------------------------- |
| Describes the expected type.                        | Stores a value shared by all instances.                  |
| Does not create an attribute.                       | Creates an actual class attribute.                       |
| Uses `field: Type`.                                 | Uses `field = value` or `field: ClassVar[Type] = value`. |
| Mainly used by IDEs, type checkers, and frameworks. | Used to store common data for the class.                 |

***

## Class Variable vs Default Value

| Class Variable                         | Default Value                                     |
| -------------------------------------- | ------------------------------------------------- |
| Shared by every instance.              | Initial value for each instance.                  |
| Belongs to the class.                  | Belongs to each object.                           |
| Accessed as `ClassName.variable`.      | Accessed through the instance.                    |
| In Pydantic, declare using `ClassVar`. | In Pydantic, declare using `field: Type = value`. |

***

## Rule of Thumb

### Plain Python

```python theme={null}
class Product:
    category = "Electronics"      # Class variable
    category: str = "Electronics" # Class variable with annotation
```

### Pydantic

```python theme={null}
class User(BaseModel):
    name: str                     # Required field
    country: str = "India"        # Default value
```

### Explicit Class Variable (Recommended)

```python theme={null}
from typing import ClassVar

class Product:
    category: ClassVar[str] = "Electronics"
```

## Using `ClassVar` makes your intent explicit and prevents frameworks like Pydantic and dataclasses from treating the attribute as an instance field.

## Practice & Exercises

To reinforce what you've learned in this section (classes/instances, static/class methods, inheritance/**init\_subclass**, encapsulation, properties, abstract base classes, and introspection), practice with these interactive notebooks:

<CardGroup cols={2}>
  <Card title="Follow-Along Practice" icon="laptop-code">
    Practice class instantiation, defining instance/class/static methods, class inheritance, custom **init\_subclass** hooks, protected/private encapsulation, properties, ABC templates, and instance/class introspection.

    [💻 VS Code](vscode://file/Users/sivaprasad/Downloads/python%20material/fastapi-course/public/notebooks/basics_exercises/OOP_Classes_Practice.ipynb) | [🚀 Colab](https://colab.research.google.com/github/prasad230776/fastapi-course/blob/master/public/notebooks/basics_exercises/OOP_Classes_Practice.ipynb) | <a href="/public/notebooks/basics_exercises/OOP_Classes_Practice.ipynb" download>📥 Download</a>
  </Card>

  <Card title="Practice Exercises" icon="pen-to-square">
    Test your knowledge with hands-on exercises including car factories, string parsers, department managers, secure bank accounts, notifications dispatchers, and class metadata scanners.

    [💻 VS Code](vscode://file/Users/sivaprasad/Downloads/python%20material/fastapi-course/public/notebooks/basics_exercises/OOP_Classes_Exercises.ipynb) | [🚀 Colab](https://colab.research.google.com/github/prasad230776/fastapi-course/blob/master/public/notebooks/basics_exercises/OOP_Classes_Exercises.ipynb) | <a href="/public/notebooks/basics_exercises/OOP_Classes_Exercises.ipynb" download>📥 Download</a>
  </Card>
</CardGroup>
